Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
badcc7bbf1 | ||
|
|
2d90498d80 | ||
|
|
c83037a3f7 | ||
|
|
95b7a0781e | ||
|
|
8c337704df | ||
|
|
780275f87e | ||
|
|
0db0b8a01c | ||
|
|
be2b3cd9b3 | ||
|
|
2fa1d88c22 | ||
|
|
8575a52ec1 | ||
|
|
f5681c248d | ||
|
|
06634a727b | ||
|
|
d1712286fd | ||
|
|
46053a793e | ||
|
|
4e37fbd2b5 | ||
|
|
79bcff0d26 | ||
|
|
ca8a01aced | ||
|
|
8f55aab848 | ||
|
|
79150ddca8 | ||
|
|
9b4f05d66a | ||
|
|
f7de3c026d | ||
|
|
8aa3416260 | ||
|
|
5051e8b2f8 | ||
|
|
dafe2583fe | ||
|
|
b124c75e4b | ||
|
|
f076049b68 | ||
|
|
0b52c40661 | ||
|
|
60dab4fa5b | ||
|
|
257a9fc5b9 | ||
|
|
82e78d548d | ||
|
|
bf167639ff | ||
|
|
503cb20c83 | ||
|
|
1a84ea1506 | ||
|
|
4f5c2ffa98 | ||
|
|
3adf04ff6d | ||
|
|
5e9da1e492 | ||
|
|
89bc8d4ccc | ||
|
|
53435e27b4 | ||
|
|
be6a2fe873 | ||
|
|
375d821fbd | ||
|
|
6c7076b39e | ||
|
|
0b1ed82cf7 | ||
|
|
917617f657 | ||
|
|
971f2142d4 | ||
|
|
6a3e567010 | ||
|
|
6570e3327f | ||
|
|
4a2edb2291 | ||
|
|
bffdb151ea | ||
|
|
59c9a599fc | ||
|
|
5fa78b622f | ||
|
|
a3e65515f6 | ||
|
|
74d5a98f6d | ||
|
|
42a2cff7da | ||
|
|
9400d6f7c1 | ||
|
|
70d59642b1 | ||
|
|
7c7b8873de | ||
|
|
bb7dee63e4 | ||
|
|
f10f5868bb | ||
|
|
d2642eea92 | ||
|
|
9e579a7cf4 | ||
|
|
a53af2a328 | ||
|
|
2e1b98007e | ||
|
|
6cb200bc39 | ||
|
|
3c840aca46 | ||
|
|
5119a51012 | ||
|
|
395670dd60 | ||
|
|
1da1388079 | ||
|
|
4d8da4dce6 | ||
|
|
cb7f4d9806 | ||
|
|
32ad99f620 | ||
|
|
1f63e2d24b | ||
|
|
60a24c03b0 | ||
|
|
adb6aed94a | ||
|
|
72f6dc2bd0 | ||
|
|
b5e060980a | ||
|
|
11ac5cbfc7 | ||
|
|
2bf7fe1239 | ||
|
|
aafbc33664 | ||
|
|
cdcbde29f4 | ||
|
|
9ee1f286a7 | ||
|
|
298791a4e5 | ||
|
|
d00407b410 | ||
|
|
d30b2cb4c6 | ||
|
|
193fb75ec7 | ||
|
|
2e5249d5c5 | ||
|
|
ccf6598ddc | ||
|
|
f40947c688 | ||
|
|
7b8086b25b | ||
|
|
4904e1c2a4 | ||
|
|
0e70a88e13 | ||
|
|
e2575f8a9a | ||
|
|
459a232c26 | ||
|
|
fd5d8a06ce | ||
|
|
bcd4b059d5 | ||
|
|
3c8f0c9e22 | ||
|
|
6c66898fc9 | ||
|
|
a91d750b5c | ||
|
|
aaf701f364 | ||
|
|
d08996e509 | ||
|
|
58987edd12 | ||
|
|
7fc3d2443b | ||
|
|
83fa34eb75 | ||
|
|
c8b4124535 | ||
|
|
19e6d8575f | ||
|
|
ebcc693ab4 | ||
|
|
84f29de969 | ||
|
|
1490f202b3 | ||
|
|
0c1537446f | ||
|
|
a237037ade | ||
|
|
534224c212 | ||
|
|
8e43b185fe | ||
|
|
e0c7292d35 | ||
|
|
bcf8710019 | ||
|
|
53b5c1eee8 | ||
|
|
271c50afa3 | ||
|
|
8f86e768d0 | ||
|
|
e83722cd88 | ||
|
|
89ef69ba28 | ||
|
|
8f3088bcb3 | ||
|
|
fe5becf235 | ||
|
|
63d135071d | ||
|
|
55c7109b98 | ||
|
|
1597f0fac0 | ||
|
|
05cd77e963 | ||
|
|
5ae53acbb9 | ||
|
|
09acc6c7d8 | ||
|
|
d31e0ebc7e | ||
|
|
1eefaa104e | ||
|
|
c92f86d602 | ||
|
|
df8381b7c6 | ||
|
|
8454ee4306 | ||
|
|
60b870488c | ||
|
|
bf9342847f | ||
|
|
b047ee8fda | ||
|
|
b6fb15cc10 | ||
|
|
df00c41237 | ||
|
|
b7a4259f29 | ||
|
|
335e8ca498 | ||
|
|
34842787e3 | ||
|
|
eb62bd0b6f | ||
|
|
2a35e83939 |
|
|
@ -9,5 +9,5 @@ indent_style = space
|
|||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{php,js,md,styl}]
|
||||
[*.{php,tpl,js,md,styl}]
|
||||
indent_size = 4
|
||||
|
|
|
|||
87
.env.example
87
.env.example
|
|
@ -1,5 +1,5 @@
|
|||
###################################
|
||||
# Blessing Skin Server V3 #
|
||||
# Blessing Skin Server v3 #
|
||||
# Configuration #
|
||||
###################################
|
||||
|
||||
|
|
@ -7,72 +7,71 @@
|
|||
# 请访问 http://t.cn/RRZ1OWd 查看中文教程
|
||||
|
||||
# Be sure to disable debug at production environment!
|
||||
APP_DEBUG = false
|
||||
APP_ENV = production
|
||||
APP_ENV = production
|
||||
APP_DEBUG = false
|
||||
|
||||
# =========================
|
||||
# = Database =
|
||||
# =========================
|
||||
DB_CONNECTION = mysql
|
||||
DB_HOST = localhost
|
||||
DB_PORT = 3306
|
||||
DB_DATABASE = blessing-skin
|
||||
DB_USERNAME = username
|
||||
DB_PASSWORD = secret
|
||||
# =========================
|
||||
# Database Configuration
|
||||
DB_CONNECTION = mysql
|
||||
DB_HOST = localhost
|
||||
DB_PORT = 3306
|
||||
DB_DATABASE = skin
|
||||
DB_USERNAME = username
|
||||
DB_PASSWORD = secret
|
||||
|
||||
# Table Prefix
|
||||
#
|
||||
# Enable if you want to install multiple Blessing Skin Server into one database.
|
||||
# It should only contains characters, numbers and underscores.
|
||||
# Change if you want to install multiple BS instances into one database.
|
||||
# The prefix may only contain letters, numbers, and underscores.
|
||||
#
|
||||
DB_PREFIX = null
|
||||
|
||||
# Encrypt Method for Passwords.
|
||||
# Hash Algorithm for Passwords
|
||||
#
|
||||
# Available values:
|
||||
# - PHP_PASSWORD_HASH,
|
||||
# - (SALTED2)MD5,
|
||||
# - (SALTED2)SHA256,
|
||||
# - (SALTED2)SHA512,
|
||||
# - CrazyCrypt1
|
||||
# - PHP_PASSWORD_HASH
|
||||
# - MD5, SALTED2MD5
|
||||
# - SHA256, SALTED2SHA256
|
||||
# - SHA512, SALTED2SHA512
|
||||
#
|
||||
# New sites are *highly* recommend to use PHP_PASSWORD_HASH.
|
||||
# New sites are *highly* recommended to use PHP_PASSWORD_HASH.
|
||||
#
|
||||
PWD_METHOD = PHP_PASSWORD_HASH
|
||||
|
||||
# Salt
|
||||
# Change it to any random string to secure your passwords & tokens.
|
||||
#
|
||||
# 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
|
||||
SALT = bs893tnok114514tdkr1919yj810snpi
|
||||
|
||||
# App Key should be setted to any random, *32 character* string,
|
||||
# App Key
|
||||
#
|
||||
# Should be formatted as `"base64:".base64_encode(random_bytes(32))`.
|
||||
# Run [php artisan key:generate] to generate a new app key,
|
||||
# otherwise all the encrypted strings will not be safe.
|
||||
#
|
||||
# You can run [php artisan key:generate] to generate a new key.
|
||||
#
|
||||
APP_KEY = base64:gkb/zouNF6UOSfnr/o+izVMS57WQS3+62YqZBuDyBhU=
|
||||
APP_KEY = base64:MfnScX0W/ViN8bZtRt0P481rWP3igcOK80QstjbXUxI=
|
||||
|
||||
# Mail Configurations
|
||||
# Leave MAIL_HOST empty to disable password resetting
|
||||
MAIL_DRIVER = smtp
|
||||
MAIL_HOST = null
|
||||
MAIL_PORT = 465
|
||||
MAIL_USERNAME = null
|
||||
MAIL_PASSWORD = null
|
||||
# Mail Configuration
|
||||
#
|
||||
# Leave MAIL_DRIVER empty to disable features involving sending emails.
|
||||
#
|
||||
MAIL_DRIVER = smtp
|
||||
MAIL_HOST = null
|
||||
MAIL_PORT = 465
|
||||
MAIL_USERNAME = null
|
||||
MAIL_PASSWORD = null
|
||||
MAIL_ENCRYPTION = ssl
|
||||
|
||||
# Change below lines only if you know what they mean!
|
||||
CACHE_DRIVER = file
|
||||
SESSION_DRIVER = file
|
||||
QUEUE_DRIVER = sync
|
||||
FS_DRIVER = local
|
||||
CACHE_DRIVER = file
|
||||
SESSION_DRIVER = file
|
||||
QUEUE_DRIVER = sync
|
||||
FS_DRIVER = local
|
||||
|
||||
REDIS_HOST = 127.0.0.1
|
||||
REDIS_PASSWORD = null
|
||||
REDIS_PORT = 6379
|
||||
REDIS_HOST = 127.0.0.1
|
||||
REDIS_PASSWORD = null
|
||||
REDIS_PORT = 6379
|
||||
|
||||
PLUGINS_DIR = null
|
||||
PLUGINS_URL = null
|
||||
PLUGINS_DIR = null
|
||||
PLUGINS_URL = null
|
||||
|
|
|
|||
54
.env.testing
54
.env.testing
|
|
@ -1,36 +1,38 @@
|
|||
###################################
|
||||
# Blessing Skin Server V3 #
|
||||
# Blessing Skin Server v3 #
|
||||
# Testing Configuration #
|
||||
###################################
|
||||
|
||||
APP_DEBUG = true
|
||||
APP_ENV = testing
|
||||
APP_ENV = testing
|
||||
APP_DEBUG = true
|
||||
|
||||
DB_CONNECTION = mysql
|
||||
# Connect to MySQL server via TCP/IP instead of Unix Domain Socket
|
||||
DB_HOST = 127.0.0.1
|
||||
DB_PORT = 3306
|
||||
DB_DATABASE = test
|
||||
DB_USERNAME = root
|
||||
DB_PASSWORD = null
|
||||
DB_PREFIX = null
|
||||
PWD_METHOD = PHP_PASSWORD_HASH
|
||||
DB_CONNECTION = mysql
|
||||
DB_HOST = 127.0.0.1
|
||||
DB_PORT = 3306
|
||||
DB_DATABASE = test
|
||||
DB_USERNAME = root
|
||||
DB_PASSWORD = null
|
||||
DB_PREFIX = null
|
||||
|
||||
SALT = c67709dd8b7b733aca0d570681fe96cf
|
||||
APP_KEY = base64:eVX/xzF5NhpGB2luswliFx9XSBsbbAP21wOi68X/P34=
|
||||
PWD_METHOD = PHP_PASSWORD_HASH
|
||||
SALT = bs893tnok114514tdkr1919yj810snpi
|
||||
APP_KEY = base64:MfnScX0W/ViN8bZtRt0P481rWP3igcOK80QstjbXUxI=
|
||||
|
||||
MAIL_DRIVER = smtp
|
||||
MAIL_HOST = localhost
|
||||
MAIL_PORT = 465
|
||||
MAIL_USERNAME = null
|
||||
MAIL_PASSWORD = null
|
||||
MAIL_DRIVER = smtp
|
||||
MAIL_HOST = localhost
|
||||
MAIL_PORT = 465
|
||||
MAIL_USERNAME = null
|
||||
MAIL_PASSWORD = null
|
||||
MAIL_ENCRYPTION = ssl
|
||||
|
||||
CACHE_DRIVER = array
|
||||
SESSION_DRIVER = array
|
||||
QUEUE_DRIVER = sync
|
||||
FS_DRIVER = memory
|
||||
CACHE_DRIVER = array
|
||||
SESSION_DRIVER = array
|
||||
QUEUE_DRIVER = sync
|
||||
FS_DRIVER = memory
|
||||
|
||||
REDIS_HOST = 127.0.0.1
|
||||
REDIS_PASSWORD = null
|
||||
REDIS_PORT = 6379
|
||||
REDIS_HOST = 127.0.0.1
|
||||
REDIS_PASSWORD = null
|
||||
REDIS_PORT = 6379
|
||||
|
||||
PLUGINS_DIR = null
|
||||
PLUGINS_URL = null
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
node_modules/
|
||||
resources/assets/dist/
|
||||
resources/assets/src/vendor/
|
||||
gulpfile.js
|
||||
|
|
|
|||
2
.gitattributes
vendored
2
.gitattributes
vendored
|
|
@ -1,2 +0,0 @@
|
|||
* text=auto
|
||||
*.tpl linguist-language=php
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,10 +1,8 @@
|
|||
.DS_Store
|
||||
.env
|
||||
.sass-cache
|
||||
coverage
|
||||
.idea/
|
||||
vendor/*
|
||||
storage/textures/*
|
||||
storage/update_cache/*
|
||||
node_modules/*
|
||||
yarn-error.log
|
||||
|
|
|
|||
28
.htaccess
28
.htaccess
|
|
@ -1,10 +1,26 @@
|
|||
RewriteEngine On
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteBase /
|
||||
RewriteEngine On
|
||||
|
||||
RewriteRule (^\.|/\.) - [F]
|
||||
# You may need to uncomment the following line for some hosting environments,
|
||||
# if you have installed to a subdirectory, enter the name here also.
|
||||
#
|
||||
# RewriteBase /
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
# Black list protected files
|
||||
RewriteRule (^\.|/\.) - [F]
|
||||
RewriteRule ^storage/.* - [F]
|
||||
|
||||
RewriteRule ^.*$ index.php [L]
|
||||
# Redirect trailing slashes if not a folder
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Handle front controller
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ git:
|
|||
cache:
|
||||
directories:
|
||||
- vendor
|
||||
- plugins
|
||||
- node_modules
|
||||
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace App\Exceptions;
|
|||
use Exception;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
use Illuminate\Session\TokenMismatchException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
|
@ -22,6 +23,7 @@ class Handler extends ExceptionHandler
|
|||
protected $dontReport = [
|
||||
HttpException::class,
|
||||
ModelNotFoundException::class,
|
||||
TokenMismatchException::class,
|
||||
ValidationException::class,
|
||||
PrettyPageException::class,
|
||||
MethodNotAllowedHttpException::class,
|
||||
|
|
@ -52,7 +54,14 @@ class Handler extends ExceptionHandler
|
|||
}
|
||||
|
||||
if ($e instanceof MethodNotAllowedHttpException) {
|
||||
abort(403, 'Method not allowed.');
|
||||
abort(403, trans('errors.http.method-not-allowed'));
|
||||
}
|
||||
|
||||
if ($e instanceof TokenMismatchException) {
|
||||
if (request()->ajax()) {
|
||||
return json(trans('errors.http.csrf-token-mismatch'), 1);
|
||||
}
|
||||
abort(403, trans('errors.http.csrf-token-mismatch'));
|
||||
}
|
||||
|
||||
if ($e instanceof PrettyPageException) {
|
||||
|
|
@ -79,6 +88,40 @@ class Handler extends ExceptionHandler
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the given HttpException.
|
||||
*
|
||||
* @param \Symfony\Component\HttpKernel\Exception\HttpException $e
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function renderHttpException(HttpException $e)
|
||||
{
|
||||
$status = $e->getStatusCode();
|
||||
$message = $e->getMessage();
|
||||
|
||||
// Get message from exception itself > translation > standard status texts
|
||||
if (! $message) {
|
||||
if (trans()->has($transKey = "errors.http.msg-{$status}")) {
|
||||
$message = trans($transKey);
|
||||
} else {
|
||||
$message = array_get(Response::$statusTexts, $status, "Status code: $status");
|
||||
}
|
||||
}
|
||||
|
||||
if (request()->ajax()) {
|
||||
return response($message, $status, $e->getHeaders());
|
||||
}
|
||||
|
||||
if (view()->exists("errors.{$status}")) {
|
||||
return response()->view("errors.{$status}", ['exception' => $e], $status, $e->getHeaders());
|
||||
}
|
||||
|
||||
return response()->view('errors.http', [
|
||||
'title' => "HTTP {$status}",
|
||||
'message' => $message
|
||||
], $status, $e->getHeaders());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception using Whoops.
|
||||
*
|
||||
|
|
@ -91,28 +134,28 @@ class Handler extends ExceptionHandler
|
|||
{
|
||||
$whoops = new \Whoops\Run;
|
||||
$handler = (request()->isMethod('GET')) ?
|
||||
new \Whoops\Handler\PrettyPageHandler : new \Whoops\Handler\PlainTextHandler;
|
||||
new \Whoops\Handler\PrettyPageHandler : new \Whoops\Handler\PlainTextHandler;
|
||||
$whoops->pushHandler($handler);
|
||||
|
||||
return new Response(
|
||||
$whoops->handleException($e),
|
||||
$code,
|
||||
$headers
|
||||
);
|
||||
return response($whoops->handleException($e), $code, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception in a short word.
|
||||
* Render an exception with error messages only.
|
||||
*
|
||||
* @param Exception $e
|
||||
* @param int $code
|
||||
* @param array $headers
|
||||
* @return Response
|
||||
*/
|
||||
protected function renderExceptionInBrief(Exception $e)
|
||||
protected function renderExceptionInBrief(Exception $e, $code = 200, $headers = [])
|
||||
{
|
||||
if (request()->isMethod('GET') && !request()->ajax()) {
|
||||
return response()->view('errors.exception', ['message' => $e->getMessage()]);
|
||||
} else {
|
||||
return $e->getMessage();
|
||||
if (request()->ajax()) {
|
||||
return response($e->getMessage(), $code, $headers);
|
||||
}
|
||||
|
||||
return response()->view('errors.exception', [
|
||||
'message' => $e->getMessage()
|
||||
], $code, $headers);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Option;
|
||||
use Schema;
|
||||
use Datatables;
|
||||
use App\Events;
|
||||
use Carbon\Carbon;
|
||||
|
|
@ -95,9 +96,8 @@ class AdminController extends Controller
|
|||
|
||||
$form->textarea('copyright_text')->rows(6)->description();
|
||||
|
||||
})->with('copyright_text',
|
||||
option('copyright_text_'.config('app.locale'), option('copyright_text'))
|
||||
)->handle(function () {
|
||||
})->handle(function () {
|
||||
Option::set('copyright_prefer_'.config('app.locale'), request('copyright_prefer'));
|
||||
Option::set('copyright_text_'.config('app.locale'), request('copyright_text'));
|
||||
});
|
||||
|
||||
|
|
@ -172,6 +172,8 @@ class AdminController extends Controller
|
|||
});
|
||||
|
||||
$form->checkbox('user_can_register')->label();
|
||||
$form->checkbox('register_with_player_name')->label();
|
||||
$form->checkbox('require_verification')->label();
|
||||
|
||||
$form->text('regs_per_ip');
|
||||
|
||||
|
|
@ -204,6 +206,10 @@ class AdminController extends Controller
|
|||
|
||||
$form->checkbox('auto_del_invalid_texture')->label()->hint();
|
||||
|
||||
$form->checkbox('allow_downloading_texture')->label();
|
||||
|
||||
$form->text('texture_name_regexp')->hint()->placeholder();
|
||||
|
||||
$form->textarea('comment_script')->rows(6)->description();
|
||||
|
||||
$form->checkbox('allow_sending_statistics')->label()->hint();
|
||||
|
|
@ -223,7 +229,7 @@ class AdminController extends Controller
|
|||
{
|
||||
$form->checkbox('force_ssl')->label()->hint();
|
||||
$form->checkbox('auto_detect_asset_url')->label()->description();
|
||||
$form->checkbox('return_200_when_notfound')->label()->description();
|
||||
$form->checkbox('return_204_when_notfound')->label()->description();
|
||||
|
||||
$form->text('cache_expire_time')->hint(OptionForm::AUTO_DETECT);
|
||||
|
||||
|
|
@ -245,24 +251,27 @@ class AdminController extends Controller
|
|||
|
||||
public function getUserData(Request $request)
|
||||
{
|
||||
$users = collect();
|
||||
// Make it still basically functional if new columns are not ready
|
||||
$columns = array_merge([
|
||||
'uid', 'email', 'nickname', 'score', 'permission', 'register_at'
|
||||
], Schema::hasColumn('users', 'verified') ? ['verified'] : []);
|
||||
|
||||
$query = User::select($columns);
|
||||
|
||||
if ($request->has('uid')) {
|
||||
$users = User::select(['uid', 'email', 'nickname', 'score', 'permission', 'register_at'])
|
||||
->where('uid', intval($request->input('uid')));
|
||||
} else {
|
||||
$users = User::select(['uid', 'email', 'nickname', 'score', 'permission', 'register_at']);
|
||||
$query->where('uid', $request->get('uid'));
|
||||
}
|
||||
|
||||
return Datatables::of($users)->editColumn('email', function ($user) {
|
||||
return $user->email ?: 'EMPTY';
|
||||
})
|
||||
->setRowId('uid')
|
||||
->addColumn('operations', app('user.current')->getPermission())
|
||||
->addColumn('players_count', function ($user) {
|
||||
return Player::where('uid', $user->uid)->count();
|
||||
})
|
||||
->make(true);
|
||||
return Datatables::of($query)
|
||||
->setRowId('uid')
|
||||
->editColumn('email', function ($user) {
|
||||
return $user->email ?: 'EMPTY';
|
||||
})
|
||||
->addColumn('operations', app('user.current')->getPermission())
|
||||
->addColumn('players_count', function ($user) {
|
||||
return Player::where('uid', $user->uid)->count();
|
||||
})
|
||||
->make(true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -278,15 +287,19 @@ class AdminController extends Controller
|
|||
|
||||
public function getPlayerData(Request $request)
|
||||
{
|
||||
$players = collect();
|
||||
$query = Player::select([
|
||||
'pid', 'uid', 'player_name', 'preference', 'tid_steve', 'tid_alex', 'tid_cape', 'last_modified'
|
||||
]);
|
||||
|
||||
if ($request->has('uid')) {
|
||||
$players = Player::select(['pid', 'uid', 'player_name', 'preference', 'tid_steve', 'tid_alex', 'tid_cape', 'last_modified'])
|
||||
->where('uid', intval($request->input('uid')));
|
||||
} else {
|
||||
$players = Player::select(['pid', 'uid', 'player_name', 'preference', 'tid_steve', 'tid_alex', 'tid_cape', 'last_modified']);
|
||||
$query->where('uid', $request->get('uid'));
|
||||
}
|
||||
|
||||
return Datatables::of($players)->setRowId('pid')->make(true);
|
||||
if ($request->has('pid')) {
|
||||
$query->where('pid', $request->get('pid'));
|
||||
}
|
||||
|
||||
return Datatables::of($query)->setRowId('pid')->make(true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -323,6 +336,13 @@ class AdminController extends Controller
|
|||
|
||||
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'
|
||||
|
|
@ -444,7 +464,7 @@ class AdminController extends Controller
|
|||
return json(trans('admin.players.delete.success'), 0);
|
||||
} elseif ($action == "name") {
|
||||
$this->validate($request, [
|
||||
'name' => 'required'
|
||||
'name' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max')
|
||||
]);
|
||||
|
||||
$player->rename($request->input('name'));
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ namespace App\Http\Controllers;
|
|||
use Log;
|
||||
use Mail;
|
||||
use View;
|
||||
use Utils;
|
||||
use Cache;
|
||||
use Cookie;
|
||||
use Option;
|
||||
use Schema;
|
||||
use Session;
|
||||
use App\Events;
|
||||
use App\Models\User;
|
||||
use App\Models\Player;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
use App\Services\Repositories\UserRepository;
|
||||
|
|
@ -41,7 +43,11 @@ class AuthController extends Controller
|
|||
// it will return a null value.
|
||||
$user = $users->get($identification, $authType);
|
||||
|
||||
if (session('login_fails', 0) > 3) {
|
||||
// Require CAPTCHA if user fails to login more than 3 times
|
||||
$loginFailsCacheKey = sha1('login_fails_'.get_client_ip());
|
||||
$loginFails = (int) Cache::get($loginFailsCacheKey, 0);
|
||||
|
||||
if ($loginFails > 3) {
|
||||
if (strtolower($request->input('captcha')) != strtolower(session('phrase')))
|
||||
return json(trans('auth.validation.captcha'), 1);
|
||||
}
|
||||
|
|
@ -50,7 +56,7 @@ class AuthController extends Controller
|
|||
return json(trans('auth.validation.user'), 2);
|
||||
} else {
|
||||
if ($user->verifyPassword($request->input('password'))) {
|
||||
Session::forget('login_fails');
|
||||
Cache::forget($loginFailsCacheKey);
|
||||
|
||||
Session::put('uid' , $user->uid);
|
||||
Session::put('token', $user->getToken());
|
||||
|
|
@ -68,10 +74,11 @@ class AuthController extends Controller
|
|||
->withCookie('uid', $user->uid, $time)
|
||||
->withCookie('token', $user->getToken(), $time);
|
||||
} else {
|
||||
Session::put('login_fails', session('login_fails', 0) + 1);
|
||||
// Increase the counter
|
||||
Cache::put($loginFailsCacheKey, ++$loginFails, 60);
|
||||
|
||||
return json(trans('auth.validation.password'), 1, [
|
||||
'login_fails' => session('login_fails')
|
||||
'login_fails' => $loginFails
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -106,60 +113,82 @@ class AuthController extends Controller
|
|||
if (! $this->checkCaptcha($request))
|
||||
return json(trans('auth.validation.captcha'), 1);
|
||||
|
||||
$this->validate($request, [
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|min:8|max:32',
|
||||
'nickname' => 'required|no_special_chars|max:255'
|
||||
]);
|
||||
|
||||
if (! option('user_can_register')) {
|
||||
if (! option('user_can_register'))
|
||||
return json(trans('auth.register.close'), 7);
|
||||
|
||||
// Validate nickname or player name
|
||||
$rule = option('register_with_player_name') ?
|
||||
['player_name' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max')] :
|
||||
['nickname' => 'required|no_special_chars|max:255'];
|
||||
$this->validate($request, array_merge([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|min:8|max:32'
|
||||
], $rule));
|
||||
|
||||
if (option('register_with_player_name')) {
|
||||
event(new Events\CheckPlayerExists($request->get('player_name')));
|
||||
|
||||
if (Player::where('player_name', $request->get('player_name'))->first()) {
|
||||
return json(trans('user.player.add.repeated'), 2);
|
||||
}
|
||||
}
|
||||
|
||||
// If amount of registered accounts of IP is more than allowed amounts,
|
||||
// then reject the register.
|
||||
if (User::where('ip', Utils::getClientIp())->count() < option('regs_per_ip'))
|
||||
{
|
||||
// Register a new user.
|
||||
// If the email is already registered,
|
||||
// it will return a false value.
|
||||
$user = User::register(
|
||||
$request->input('email'),
|
||||
$request->input('password'), function($user) use ($request)
|
||||
{
|
||||
$user->ip = Utils::getClientIp();
|
||||
$user->score = option('user_initial_score');
|
||||
$user->register_at = Utils::getTimeFormatted();
|
||||
$user->last_sign_at = Utils::getTimeFormatted(time() - 86400);
|
||||
$user->permission = User::NORMAL;
|
||||
$user->nickname = $request->input('nickname');
|
||||
});
|
||||
|
||||
if (! $user) {
|
||||
return json(trans('auth.register.registered'), 5);
|
||||
}
|
||||
|
||||
event(new Events\UserRegistered($user));
|
||||
|
||||
return json([
|
||||
'errno' => 0,
|
||||
'msg' => trans('auth.register.success'),
|
||||
'token' => $user->getToken(),
|
||||
]) // Set cookies
|
||||
->withCookie('uid', $user->uid, 60)
|
||||
->withCookie('token', $user->getToken(), 60);
|
||||
|
||||
} else {
|
||||
if (User::where('ip', get_client_ip())->count() >= option('regs_per_ip')) {
|
||||
return json(trans('auth.register.max', ['regs' => option('regs_per_ip')]), 7);
|
||||
}
|
||||
|
||||
// Register a new user.
|
||||
// If the email is already registered,
|
||||
// it will return a false value.
|
||||
$user = User::register(
|
||||
$request->get('email'),
|
||||
$request->get('password'), function($user) use ($request)
|
||||
{
|
||||
$user->ip = get_client_ip();
|
||||
$user->score = option('user_initial_score');
|
||||
$user->register_at = get_datetime_string();
|
||||
$user->last_sign_at = get_datetime_string(time() - 86400);
|
||||
$user->permission = User::NORMAL;
|
||||
$user->nickname = $request->get(
|
||||
option('register_with_player_name') ? 'player_name' : 'nickname'
|
||||
);
|
||||
});
|
||||
|
||||
if (! $user) {
|
||||
return json(trans('auth.register.registered'), 5);
|
||||
}
|
||||
|
||||
event(new Events\UserRegistered($user));
|
||||
|
||||
// Add player with chosen name
|
||||
if (option('register_with_player_name')) {
|
||||
$player = new Player;
|
||||
$player->uid = $user->uid;
|
||||
$player->player_name = $request->get('player_name');
|
||||
$player->preference = 'default';
|
||||
$player->last_modified = get_datetime_string();
|
||||
$player->save();
|
||||
|
||||
event(new Events\PlayerWasAdded($player));
|
||||
}
|
||||
|
||||
return json([
|
||||
'errno' => 0,
|
||||
'msg' => trans('auth.register.success'),
|
||||
'token' => $user->getToken(),
|
||||
]) // Set cookies
|
||||
->withCookie('uid', $user->uid, 60)
|
||||
->withCookie('token', $user->getToken(), 60);
|
||||
}
|
||||
|
||||
public function forgot()
|
||||
{
|
||||
if (config('mail.host') != "") {
|
||||
if (config('mail.driver') != "") {
|
||||
return view('auth.forgot');
|
||||
} else {
|
||||
throw new PrettyPageException(trans('auth.forgot.close'), 8);
|
||||
throw new PrettyPageException(trans('auth.forgot.disabled'), 8);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -168,11 +197,22 @@ class AuthController extends Controller
|
|||
if (! $this->checkCaptcha($request))
|
||||
return json(trans('auth.validation.captcha'), 1);
|
||||
|
||||
if (config('mail.host') == "")
|
||||
return json(trans('auth.forgot.close'), 1);
|
||||
if (! config('mail.driver')) {
|
||||
return json(trans('auth.forgot.disabled'), 1);
|
||||
}
|
||||
|
||||
if (Session::has('last_mail_time') && (time() - session('last_mail_time')) < 60)
|
||||
return json(trans('auth.forgot.frequent-mail'), 1);
|
||||
$rateLimit = 180;
|
||||
$lastMailCacheKey = sha1('last_mail_'.get_client_ip());
|
||||
$remain = $rateLimit + Cache::get($lastMailCacheKey, 0) - time();
|
||||
|
||||
// Rate limit
|
||||
if ($remain > 0) {
|
||||
return json([
|
||||
'errno' => 2,
|
||||
'msg' => trans('auth.forgot.frequent-mail'),
|
||||
'remain' => $remain
|
||||
]);
|
||||
}
|
||||
|
||||
// Get user instance
|
||||
$user = $users->get($request->input('email'), 'email');
|
||||
|
|
@ -182,55 +222,48 @@ class AuthController extends Controller
|
|||
|
||||
$uid = $user->uid;
|
||||
// Generate token for password resetting
|
||||
$token = base64_encode($user->getToken().substr(time(), 4, 6).str_random(16));
|
||||
|
||||
$token = generate_random_token();
|
||||
$url = Option::get('site_url')."/auth/reset?uid=$uid&token=$token";
|
||||
|
||||
try {
|
||||
Mail::send('auth.mail', ['reset_url' => $url], function ($m) use ($request) {
|
||||
$site_name = Option::get('site_name');
|
||||
Mail::send('mails.password-reset', compact('url'), function ($m) use ($request) {
|
||||
$site_name = option_localized('site_name');
|
||||
|
||||
$m->from(config('mail.username'), $site_name);
|
||||
$m->to($request->input('email'))->subject(trans('auth.mail.title', ['sitename' => $site_name]));
|
||||
$m->to($request->input('email'))->subject(trans('auth.forgot.mail.title', ['sitename' => $site_name]));
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
// Write the exception to log
|
||||
report($e);
|
||||
|
||||
Log::info("[Password Reset] Mail has been sent to [{$request->input('email')}] with token [$token]");
|
||||
} catch(\Exception $e) {
|
||||
return json(trans('auth.mail.failed', ['msg' => $e->getMessage()]), 2);
|
||||
return json(trans('auth.forgot.failed', ['msg' => $e->getMessage()]), 2);
|
||||
}
|
||||
|
||||
Session::put('last_mail_time', time());
|
||||
Cache::put("pwd_reset_token_$uid", $token, 60);
|
||||
Cache::put($lastMailCacheKey, time(), 60);
|
||||
|
||||
return json(trans('auth.mail.success'), 0);
|
||||
return json(trans('auth.forgot.success'), 0);
|
||||
}
|
||||
|
||||
public function reset(UserRepository $users, Request $request)
|
||||
{
|
||||
if ($request->has('uid') && $request->has('token')) {
|
||||
// Get user instance from repository
|
||||
$user = $users->get($request->input('uid'));
|
||||
// Retrieve token from cache
|
||||
$uid = $request->get('uid');
|
||||
$token = Cache::get("pwd_reset_token_$uid");
|
||||
|
||||
if (! $user)
|
||||
return redirect('auth/forgot')->with('msg', trans('auth.reset.invalid'));
|
||||
// Get user instance from repository
|
||||
$user = $users->get($uid);
|
||||
|
||||
// Unpack to get user token & timestamp
|
||||
$decoded = base64_decode($request->input('token'));
|
||||
$token = substr($decoded, 0, -22);
|
||||
$timestamp = substr($decoded, strlen($token), 6);
|
||||
|
||||
if ($user->getToken() != $token) {
|
||||
return redirect('auth/forgot')->with('msg', trans('auth.reset.invalid'));
|
||||
}
|
||||
|
||||
// More than 1 hour
|
||||
if ((substr(time(), 4, 6) - $timestamp) > 3600) {
|
||||
return redirect('auth/forgot')->with('msg', trans('auth.reset.expired'));
|
||||
}
|
||||
|
||||
return view('auth.reset')->with('user', $user);
|
||||
} else {
|
||||
return redirect('auth/login')->with('msg', trans('auth.check.anonymous'));
|
||||
if (! $user) {
|
||||
return redirect('auth/forgot')->with('msg', trans('auth.reset.invalid'));
|
||||
}
|
||||
|
||||
// No token exist or token mismatch (maybe expired)
|
||||
if (is_null($token) || $token != $request->get('token')) {
|
||||
return redirect('auth/forgot')->with('msg', trans('auth.reset.expired'));
|
||||
}
|
||||
|
||||
return view('auth.reset')->with('user', $user);
|
||||
}
|
||||
|
||||
public function handleReset(Request $request, UserRepository $users)
|
||||
|
|
@ -241,30 +274,50 @@ class AuthController extends Controller
|
|||
'token' => 'required',
|
||||
]);
|
||||
|
||||
$decoded = base64_decode($request->input('token'));
|
||||
$token = substr($decoded, 0, -22);
|
||||
$timestamp = intval(substr($decoded, strlen($token), 6));
|
||||
// Retrieve token from cache
|
||||
$uid = $request->get('uid');
|
||||
$token = Cache::get("pwd_reset_token_$uid");
|
||||
|
||||
$user = $users->get($request->input('uid'));
|
||||
if (! $user)
|
||||
return json(trans('auth.reset.invalid'), 1);
|
||||
// Get user instance from repository
|
||||
$user = $users->get($uid);
|
||||
|
||||
if ($user->getToken() != $token) {
|
||||
if (! $user) {
|
||||
return json(trans('auth.reset.invalid'), 1);
|
||||
}
|
||||
|
||||
// More than 1 hour
|
||||
if ((intval(substr(time(), 4, 6)) - $timestamp) > 3600) {
|
||||
// No token exist or token mismatch (maybe expired)
|
||||
if (is_null($token) || $token != $request->get('token')) {
|
||||
return json(trans('auth.reset.expired'), 1);
|
||||
}
|
||||
|
||||
$users->get($request->input('uid'))->changePasswd($request->input('password'));
|
||||
|
||||
Log::info("[Password Reset] Password of user [{$request->input('uid')}] has been changed");
|
||||
$user->changePasswd($request->get('password'));
|
||||
|
||||
return json(trans('auth.reset.success'), 0);
|
||||
}
|
||||
|
||||
public function verify(Request $request, UserRepository $users)
|
||||
{
|
||||
if (!option('require_verification') || !Schema::hasColumn('users', 'verified')) {
|
||||
throw new PrettyPageException(trans('user.verification.disabled'), 1);
|
||||
}
|
||||
|
||||
// Get user instance from repository
|
||||
$user = $users->get($request->get('uid'));
|
||||
|
||||
if (!$user || $user->verified) {
|
||||
throw new PrettyPageException(trans('auth.verify.invalid'), 1);
|
||||
}
|
||||
|
||||
if ($user->verification_token != $request->get('token')) {
|
||||
throw new PrettyPageException(trans('auth.verify.expired'), 1);
|
||||
}
|
||||
|
||||
$user->verified = true;
|
||||
$user->save();
|
||||
|
||||
return view('auth.verify');
|
||||
}
|
||||
|
||||
public function captcha()
|
||||
{
|
||||
$builder = new \Gregwar\Captcha\CaptchaBuilder;
|
||||
|
|
|
|||
162
app/Http/Controllers/MarketController.php
Normal file
162
app/Http/Controllers/MarketController.php
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Exception;
|
||||
use Datatables;
|
||||
use ZipArchive;
|
||||
use Illuminate\Http\Request;
|
||||
use Composer\Semver\Comparator;
|
||||
use App\Services\PluginManager;
|
||||
|
||||
class MarketController extends Controller
|
||||
{
|
||||
/**
|
||||
* Guzzle HTTP client.
|
||||
*
|
||||
* @var \GuzzleHttp\Client
|
||||
*/
|
||||
protected $guzzle;
|
||||
|
||||
/**
|
||||
* Default request options for Guzzle HTTP client.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guzzleConfig;
|
||||
|
||||
/**
|
||||
* Cache for plugins registry.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $registryCache;
|
||||
|
||||
public function __construct(\GuzzleHttp\Client $guzzle)
|
||||
{
|
||||
$this->guzzle = $guzzle;
|
||||
$this->guzzleConfig = [
|
||||
'headers' => ['User-Agent' => config('secure.user_agent')],
|
||||
'verify' => config('secure.certificates')
|
||||
];
|
||||
}
|
||||
|
||||
public function showMarket()
|
||||
{
|
||||
return view('admin.market');
|
||||
}
|
||||
|
||||
public function getMarketData()
|
||||
{
|
||||
$plugins = collect($this->getAllAvailablePlugins())->map(function ($item) {
|
||||
$plugin = plugin($item['name']);
|
||||
$manager = app('plugins');
|
||||
|
||||
if ($plugin) {
|
||||
$item['enabled'] = $plugin->isEnabled();
|
||||
$item['installed'] = $plugin->version;
|
||||
$item['update_available'] = Comparator::greaterThan($item['version'], $item['installed']);
|
||||
} else {
|
||||
$item['installed'] = false;
|
||||
}
|
||||
|
||||
$requirements = array_get($item, 'require', []);
|
||||
unset($item['require']);
|
||||
|
||||
$item['dependencies'] = [
|
||||
'isRequirementsSatisfied' => $manager->isRequirementsSatisfied($requirements),
|
||||
'requirements' => $requirements,
|
||||
'unsatisfiedRequirements' => $manager->getUnsatisfiedRequirements($requirements)
|
||||
];
|
||||
|
||||
return $item;
|
||||
});
|
||||
|
||||
return Datatables::of($plugins)->setRowId('plugin-{{ $name }}')->make(true);
|
||||
}
|
||||
|
||||
public function checkUpdates()
|
||||
{
|
||||
$pluginsHaveUpdate = collect($this->getAllAvailablePlugins())->filter(function ($item) {
|
||||
$plugin = plugin($item['name']);
|
||||
return $plugin && Comparator::greaterThan($item['version'], $plugin->version);
|
||||
});
|
||||
|
||||
return json([
|
||||
'available' => !$pluginsHaveUpdate->isEmpty(),
|
||||
'plugins' => array_values($pluginsHaveUpdate->all())
|
||||
]);
|
||||
}
|
||||
|
||||
public function download(Request $request, PluginManager $manager)
|
||||
{
|
||||
$name = $request->get('name');
|
||||
$metadata = $this->getPluginMetadata($name);
|
||||
|
||||
if (! $metadata) {
|
||||
return json(trans('admin.plugins.market.non-existent', ['plugin' => $name]), 1);
|
||||
}
|
||||
|
||||
// Gather plugin distribution URL
|
||||
$url = $metadata['dist']['url'];
|
||||
$filename = array_last(explode('/', $url));
|
||||
$plugins_dir = $manager->getPluginsDir();
|
||||
$tmp_path = $plugins_dir.DIRECTORY_SEPARATOR.$filename;
|
||||
|
||||
// Download
|
||||
try {
|
||||
$this->guzzle->request('GET', $url, array_merge($this->guzzleConfig, [
|
||||
'sink' => $tmp_path
|
||||
]));
|
||||
} catch (Exception $e) {
|
||||
report($e);
|
||||
return json(trans('admin.plugins.market.download-failed', ['error' => $e->getMessage()]), 2);
|
||||
}
|
||||
|
||||
// Check file's sha1 hash
|
||||
if (sha1_file($tmp_path) !== $metadata['dist']['shasum']) {
|
||||
@unlink($tmp_path);
|
||||
return json(trans('admin.plugins.market.shasum-failed'), 3);
|
||||
}
|
||||
|
||||
// Unzip
|
||||
$zip = new ZipArchive();
|
||||
$res = $zip->open($tmp_path);
|
||||
|
||||
if ($res === true) {
|
||||
if ($zip->extractTo($plugins_dir) === false) {
|
||||
return json(trans('admin.plugins.market.unzip-failed', ['error' => 'Unable to extract the file.']), 4);
|
||||
}
|
||||
} else {
|
||||
return json(trans('admin.plugins.market.unzip-failed', ['error' => $res]), 4);
|
||||
}
|
||||
$zip->close();
|
||||
@unlink($tmp_path);
|
||||
|
||||
return json(trans('admin.plugins.market.install-success'), 0);
|
||||
}
|
||||
|
||||
protected function getPluginMetadata($name)
|
||||
{
|
||||
return collect($this->getAllAvailablePlugins())->where('name', $name)->first();
|
||||
}
|
||||
|
||||
protected function getAllAvailablePlugins()
|
||||
{
|
||||
if (! $this->registryCache) {
|
||||
try {
|
||||
$pluginsJson = $this->guzzle->request(
|
||||
'GET', config('plugins.registry'), $this->guzzleConfig
|
||||
)->getBody();
|
||||
} catch (Exception $e) {
|
||||
throw new Exception(trans('admin.plugins.market.connection-error', [
|
||||
'error' => htmlentities($e->getMessage())
|
||||
]));
|
||||
}
|
||||
|
||||
$this->registryCache = json_decode($pluginsJson, true);
|
||||
}
|
||||
|
||||
return array_get($this->registryCache, 'packages', []);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ namespace App\Http\Controllers;
|
|||
|
||||
use View;
|
||||
use Event;
|
||||
use Utils;
|
||||
use Option;
|
||||
use App\Models\User;
|
||||
use App\Models\Player;
|
||||
|
|
@ -79,7 +78,7 @@ class PlayerController extends Controller
|
|||
$player->uid = $this->user->uid;
|
||||
$player->player_name = $request->input('player_name');
|
||||
$player->preference = "default";
|
||||
$player->last_modified = Utils::getTimeFormatted();
|
||||
$player->last_modified = get_datetime_string();
|
||||
$player->save();
|
||||
|
||||
event(new PlayerWasAdded($player));
|
||||
|
|
@ -150,6 +149,13 @@ class PlayerController extends Controller
|
|||
$this->player->setTexture([$fieldName => $value]);
|
||||
}
|
||||
|
||||
// When user applies an alex skin to a newly added player,
|
||||
// which textures and model preference are all default value,
|
||||
// we will automatically set the player's model preference to "slim".
|
||||
if ($this->player->preference == 'default' && $this->player->tid_steve == 0 && $this->player->tid_alex != 0) {
|
||||
$this->player->setPreference('slim');
|
||||
}
|
||||
|
||||
return json(trans('user.player.set.success', ['name' => $this->player->player_name]), 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,16 +11,6 @@ use App\Services\PluginManager;
|
|||
|
||||
class PluginController extends Controller
|
||||
{
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function showMarket()
|
||||
{
|
||||
return redirect('/')->setTargetUrl(
|
||||
'https://github.com/printempw/blessing-skin-server/wiki/Plugins'
|
||||
);
|
||||
}
|
||||
|
||||
public function showManage()
|
||||
{
|
||||
return view('admin.plugins');
|
||||
|
|
@ -71,9 +61,6 @@ class PluginController extends Controller
|
|||
|
||||
return json(trans('admin.plugins.operations.enabled', ['plugin' => $plugin->title]), 0);
|
||||
|
||||
case 'requirements':
|
||||
return json($this->getPluginDependencies($plugin));
|
||||
|
||||
case 'disable':
|
||||
$plugins->disable($name);
|
||||
|
||||
|
|
@ -98,24 +85,13 @@ class PluginController extends Controller
|
|||
|
||||
return Datatables::of($installed)
|
||||
->setRowId('plugin-{{ $name }}')
|
||||
->editColumn('title', function ($plugin) {
|
||||
return trans($plugin->title ?: 'EMPTY');
|
||||
})
|
||||
->editColumn('description', function ($plugin) {
|
||||
return trans($plugin->description ?: 'EMPTY');
|
||||
})
|
||||
->editColumn('author', function ($plugin) {
|
||||
return ['author' => trans($plugin->author ?: 'EMPTY'), 'url' => $plugin->url];
|
||||
})
|
||||
->editColumn('title', '{{ trans($title ?: "EMPTY") }}')
|
||||
->editColumn('description', '{{ trans($description ?: "EMPTY") }}')
|
||||
->addColumn('enabled', function ($plugin) { return $plugin->isEnabled(); })
|
||||
->addColumn('config', function ($plugin) { return $plugin->hasConfigView(); })
|
||||
->addColumn('dependencies', function ($plugin) {
|
||||
return $this->getPluginDependencies($plugin);
|
||||
})
|
||||
->addColumn('status', function ($plugin) {
|
||||
return trans('admin.plugins.status.'.($plugin->isEnabled() ? 'enabled' : 'disabled'));
|
||||
})
|
||||
->addColumn('operations', function ($plugin) {
|
||||
return ['enabled' => $plugin->isEnabled(), 'hasConfigView' => $plugin->hasConfigView()];
|
||||
})
|
||||
->make(true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ namespace App\Http\Controllers;
|
|||
|
||||
use Log;
|
||||
use File;
|
||||
use Utils;
|
||||
use Schema;
|
||||
use Option;
|
||||
use Storage;
|
||||
|
|
@ -39,7 +38,7 @@ class SetupController extends Controller
|
|||
|
||||
// Not installed completely
|
||||
if (count($existingTables) > 0) {
|
||||
Log::info('Remaining tables detected, exit setup wizard now', [$existingTables]);
|
||||
Log::info('[SetupWizard] Remaining tables detected, exit now', [$existingTables]);
|
||||
|
||||
$existingTables = array_map(function ($item) {
|
||||
return get_db_config()['prefix'].$item;
|
||||
|
|
@ -71,22 +70,19 @@ class SetupController extends Controller
|
|||
Artisan::call('key:random');
|
||||
Artisan::call('salt:random');
|
||||
|
||||
Log::info("[SetupWizard] Random application key & salt set successfully.", [
|
||||
'key' => config('app.key'),
|
||||
'salt' => config('secure.salt')
|
||||
]);
|
||||
Log::info('[SetupWizard] Random application key & salt set successfully');
|
||||
} else {
|
||||
// @codeCoverageIgnoreStart
|
||||
Log::warning("[SetupWizard] Failed to set application key. No write permission.");
|
||||
Log::warning('[SetupWizard] Failed to set application key since .env is not writable');
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
|
||||
// Create tables
|
||||
Artisan::call('migrate', ['--force' => true]);
|
||||
Log::info("[SetupWizard] Tables migrated.");
|
||||
Log::info('[SetupWizard] Database migrated');
|
||||
|
||||
Option::set('site_name', $request->input('site_name'));
|
||||
Option::set('site_name', $request->get('site_name'));
|
||||
|
||||
$siteUrl = url('/');
|
||||
|
||||
|
|
@ -98,23 +94,23 @@ class SetupController extends Controller
|
|||
|
||||
// Register super admin
|
||||
$user = User::register(
|
||||
$request->input('email'),
|
||||
$request->input('password'), function ($user)
|
||||
$request->get('email'),
|
||||
$request->get('password'), function ($user)
|
||||
{
|
||||
$user->ip = Utils::getClientIp();
|
||||
$user->ip = get_client_ip();
|
||||
$user->score = option('user_initial_score');
|
||||
$user->register_at = Utils::getTimeFormatted();
|
||||
$user->last_sign_at = Utils::getTimeFormatted(time() - 86400);
|
||||
$user->register_at = get_datetime_string();
|
||||
$user->last_sign_at = get_datetime_string(time() - 86400);
|
||||
$user->permission = User::SUPER_ADMIN;
|
||||
});
|
||||
Log::info("[SetupWizard] Super Admin registered.", ['user' => $user]);
|
||||
Log::info('[SetupWizard] Super administrator registered');
|
||||
|
||||
$this->createDirectories();
|
||||
Log::info("[SetupWizard] Installation completed.");
|
||||
Log::info('[SetupWizard] Installation completed');
|
||||
|
||||
return view('setup.wizard.finish')->with([
|
||||
'email' => $request->input('email'),
|
||||
'password' => $request->input('password')
|
||||
'email' => $request->get('email'),
|
||||
'password' => $request->get('password')
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +170,7 @@ class SetupController extends Controller
|
|||
try {
|
||||
Artisan::call('view:clear');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error occured when processing view:clear', [$e]);
|
||||
Log::error('[UpdateWizard] Error occured when processing view:clear', [$e]);
|
||||
|
||||
$files = collect(File::files(storage_path('framework/views')));
|
||||
$files->reject(function ($path) {
|
||||
|
|
@ -194,7 +190,8 @@ class SetupController extends Controller
|
|||
* @param bool $returnExisting
|
||||
* @return bool|array
|
||||
*/
|
||||
public static function checkTablesExist($tables = [], $returnExistingTables = false) {
|
||||
public static function checkTablesExist($tables = [], $returnExistingTables = false)
|
||||
{
|
||||
$existingTables = [];
|
||||
$tables = $tables ?: ['users', 'closets', 'players', 'textures', 'options'];
|
||||
|
||||
|
|
@ -212,6 +209,28 @@ class SetupController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given columns exist in specific table.
|
||||
* By default, we will check the columns newly added to users table in BS v3.5.0.
|
||||
*
|
||||
* @param string $table
|
||||
* @param array $columns
|
||||
* @return void
|
||||
*/
|
||||
public static function checkNewColumnsExist($table = 'users', $columns = [])
|
||||
{
|
||||
$existingColumns = [];
|
||||
$columns = $columns ?: ['verified', 'verification_token'];
|
||||
|
||||
foreach ($columns as $column) {
|
||||
if (Schema::hasColumn($table, $column)) {
|
||||
$existingColumns[] = $column;
|
||||
}
|
||||
}
|
||||
|
||||
return count($existingColumns) === count($columns);
|
||||
}
|
||||
|
||||
public static function checkDirectories()
|
||||
{
|
||||
$directories = ['storage/textures', 'plugins'];
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use View;
|
||||
use Utils;
|
||||
use Option;
|
||||
use Storage;
|
||||
use Session;
|
||||
|
|
@ -20,6 +19,23 @@ class SkinlibController extends Controller
|
|||
{
|
||||
protected $user = null;
|
||||
|
||||
/**
|
||||
* Map error code of file uploading to human-readable text.
|
||||
*
|
||||
* @see http://php.net/manual/en/features.file-upload.errors.php
|
||||
* @var array
|
||||
*/
|
||||
public static $phpFileUploadErrors = [
|
||||
0 => 'There is no error, the file uploaded with success',
|
||||
1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
|
||||
2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
|
||||
3 => 'The uploaded file was only partially uploaded',
|
||||
4 => 'No file was uploaded',
|
||||
6 => 'Missing a temporary folder',
|
||||
7 => 'Failed to write file to disk.',
|
||||
8 => 'A PHP extension stopped the file upload.',
|
||||
];
|
||||
|
||||
public function __construct(UserRepository $users)
|
||||
{
|
||||
// Try to load user by uid stored in session.
|
||||
|
|
@ -167,7 +183,7 @@ class SkinlibController extends Controller
|
|||
$t->size = ceil($request->file('file')->getSize() / 1024);
|
||||
$t->public = ($request->input('public') == 'true') ? "1" : "0";
|
||||
$t->uploader = $this->user->uid;
|
||||
$t->upload_at = Utils::getTimeFormatted();
|
||||
$t->upload_at = get_datetime_string();
|
||||
|
||||
$cost = $t->size * (($t->public == "1") ? Option::get('score_per_storage') : Option::get('private_score_per_storage'));
|
||||
$cost += option('score_per_closet_item');
|
||||
|
|
@ -295,6 +311,35 @@ class SkinlibController extends Controller
|
|||
}
|
||||
} // @codeCoverageIgnore
|
||||
|
||||
public function model(Request $request) {
|
||||
$this->validate($request, [
|
||||
'tid' => 'required|integer',
|
||||
'model' => 'required|in:steve,alex,cape'
|
||||
]);
|
||||
|
||||
$t = Texture::find($request->input('tid'));
|
||||
|
||||
if (! $t)
|
||||
return json(trans('skinlib.non-existent'), 1);
|
||||
|
||||
if ($t->uploader != $this->user->uid && !$this->user->isAdmin())
|
||||
return json(trans('skinlib.no-permission'), 1);
|
||||
|
||||
$duplicate = Texture::where('hash', $t->hash)
|
||||
->where('type', $request->input('model'))
|
||||
->where('tid', '<>', $t->tid)
|
||||
->first();
|
||||
|
||||
if ($duplicate && $duplicate->public) {
|
||||
return json(trans('skinlib.model.duplicate', ['tid' => $duplicate->tid]), 1);
|
||||
}
|
||||
|
||||
$t->type = $request->input('model');
|
||||
$t->save();
|
||||
|
||||
return json(trans('skinlib.model.success', ['model' => request('model')]), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Uploaded Files
|
||||
*
|
||||
|
|
@ -305,12 +350,12 @@ class SkinlibController extends Controller
|
|||
{
|
||||
if ($file = $request->files->get('file')) {
|
||||
if ($file->getError() !== UPLOAD_ERR_OK) {
|
||||
return json(Utils::convertUploadFileError($file->getError()), $file->getError());
|
||||
return json(static::$phpFileUploadErrors[$file->getError()], $file->getError());
|
||||
}
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'name' => 'required|no_special_chars',
|
||||
'name' => 'required|'.(option('texture_name_regexp') ? 'texture_name_regexp' : 'no_special_chars'),
|
||||
'file' => 'required|max:'.option('max_upload_file_size'),
|
||||
'public' => 'required'
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use Option;
|
|||
use Storage;
|
||||
use Response;
|
||||
use Minecraft;
|
||||
use Exception;
|
||||
use Carbon\Carbon;
|
||||
use App\Models\User;
|
||||
use App\Models\Player;
|
||||
|
|
@ -16,6 +17,7 @@ use App\Events\GetSkinPreview;
|
|||
use App\Events\GetAvatarPreview;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
use App\Services\Repositories\UserRepository;
|
||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
||||
|
||||
class TextureController extends Controller
|
||||
{
|
||||
|
|
@ -38,10 +40,8 @@ class TextureController extends Controller
|
|||
$content = $player->getJsonProfile(Option::get('api_type'));
|
||||
}
|
||||
|
||||
return Response::rawJson($content, 200, [
|
||||
'Last-Modified' => Carbon::createFromTimestamp(
|
||||
$player->getLastModified()
|
||||
)->format('D, d M Y H:i:s \G\M\T')
|
||||
return Response::jsonProfile($content, 200, [
|
||||
'Last-Modified' => strtotime($player->last_modified)
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -50,16 +50,21 @@ class TextureController extends Controller
|
|||
return $this->json($player_name, $api);
|
||||
}
|
||||
|
||||
public function texture($hash) {
|
||||
if (Storage::disk('textures')->has($hash)) {
|
||||
return Response::png(Storage::disk('textures')->get($hash), 200, [
|
||||
'Last-Modified' => Storage::disk('textures')->lastModified($hash),
|
||||
'Accept-Ranges' => 'bytes',
|
||||
'Content-Length' => Storage::disk('textures')->size($hash),
|
||||
]);
|
||||
} else {
|
||||
return abort(404);
|
||||
public function texture($hash, $headers = [], $message = '') {
|
||||
try {
|
||||
if (Storage::disk('textures')->has($hash)) {
|
||||
return Response::png(Storage::disk('textures')->get($hash), 200, array_merge([
|
||||
'Last-Modified' => Storage::disk('textures')->lastModified($hash),
|
||||
'Accept-Ranges' => 'bytes',
|
||||
'Content-Length' => Storage::disk('textures')->size($hash),
|
||||
], $headers));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Let it fallback to 404
|
||||
report($e);
|
||||
}
|
||||
|
||||
return abort(404, $message);
|
||||
}
|
||||
|
||||
public function textureWithApi($api, $hash) {
|
||||
|
|
@ -98,16 +103,9 @@ class TextureController extends Controller
|
|||
$player = $this->getPlayerInstance($player_name);
|
||||
|
||||
if ($hash = $player->getTexture($type)) {
|
||||
if (Storage::disk('textures')->has($hash)) {
|
||||
// Cache friendly
|
||||
return Response::png(Storage::disk('textures')->read($hash), 200, [
|
||||
'Last-Modified' => $player->getLastModified(),
|
||||
'Accept-Ranges' => 'bytes',
|
||||
'Content-Length' => Storage::disk('textures')->size($hash),
|
||||
]);
|
||||
} else {
|
||||
abort(404, trans('general.texture-deleted'));
|
||||
}
|
||||
return $this->texture($hash, [
|
||||
'Last-Modified' => strtotime($player->last_modified)
|
||||
], trans('general.texture-deleted'));
|
||||
} else {
|
||||
abort(404, trans('general.texture-not-uploaded', ['type' => $type]));
|
||||
}
|
||||
|
|
@ -121,12 +119,14 @@ class TextureController extends Controller
|
|||
$tid = $user->getAvatarId();
|
||||
|
||||
if ($t = Texture::find($tid)) {
|
||||
if (Storage::disk('textures')->has($t->hash)) {
|
||||
$responses = event(new GetAvatarPreview($t, $size));
|
||||
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
|
||||
}
|
||||
|
||||
if (isset($responses[0]) && $responses[0] instanceof \Symfony\Component\HttpFoundation\Response) {
|
||||
return $responses[0]; // @codeCoverageIgnore
|
||||
} else {
|
||||
$png = Minecraft::generateAvatarFromSkin(Storage::disk('textures')->read($t->hash), $size);
|
||||
|
||||
ob_start();
|
||||
|
|
@ -136,7 +136,11 @@ class TextureController extends Controller
|
|||
ob_end_clean();
|
||||
|
||||
return Response::png($image);
|
||||
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Let it fallback to default avatar
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -159,12 +163,14 @@ class TextureController extends Controller
|
|||
public function preview($tid, $size = 250)
|
||||
{
|
||||
if ($t = Texture::find($tid)) {
|
||||
if (Storage::disk('textures')->has($t->hash)) {
|
||||
$responses = event(new GetSkinPreview($t, $size));
|
||||
try {
|
||||
if (Storage::disk('textures')->has($t->hash)) {
|
||||
$responses = event(new GetSkinPreview($t, $size));
|
||||
|
||||
if (isset($responses[0]) && $responses[0] instanceof SymfonyResponse) {
|
||||
return $responses[0]; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
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") {
|
||||
|
|
@ -181,6 +187,10 @@ class TextureController extends Controller
|
|||
|
||||
return Response::png($image);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Let it fallback to default preview
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -202,16 +212,10 @@ class TextureController extends Controller
|
|||
|
||||
public function raw($tid) {
|
||||
if ($t = Texture::find($tid)) {
|
||||
|
||||
if (Storage::disk('textures')->has($t->hash)) {
|
||||
return Response::png(Storage::disk('textures')->get($t->hash));
|
||||
} else {
|
||||
return abort(404, trans('general.texture-deleted'));
|
||||
}
|
||||
return $this->texture($t->hash);
|
||||
} else {
|
||||
return abort(404, trans('skinlib.non-existent'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function getPlayerInstance($player_name)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Arr;
|
||||
use Log;
|
||||
use Utils;
|
||||
use File;
|
||||
use Cache;
|
||||
use Option;
|
||||
use Storage;
|
||||
use Exception;
|
||||
use ZipArchive;
|
||||
use App\Services\OptionForm;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -15,19 +15,58 @@ use Composer\Semver\Comparator;
|
|||
|
||||
class UpdateController extends Controller
|
||||
{
|
||||
public $currentVersion;
|
||||
/**
|
||||
* Current application version.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $currentVersion;
|
||||
|
||||
public $latestVersion;
|
||||
/**
|
||||
* Latest application version in update source.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $latestVersion;
|
||||
|
||||
public $updateSource;
|
||||
/**
|
||||
* Where to get information of new application versions.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $updateSource;
|
||||
|
||||
/**
|
||||
* Updates information fetched from update source.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $updateInfo;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->updateSource = option('update_source');
|
||||
/**
|
||||
* Guzzle HTTP client.
|
||||
*
|
||||
* @var \GuzzleHttp\Client
|
||||
*/
|
||||
protected $guzzle;
|
||||
|
||||
/**
|
||||
* Default request options for Guzzle HTTP client.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guzzleConfig;
|
||||
|
||||
public function __construct(\GuzzleHttp\Client $guzzle)
|
||||
{
|
||||
$this->updateSource = config('app.update_source');
|
||||
$this->currentVersion = config('app.version');
|
||||
|
||||
$this->guzzle = $guzzle;
|
||||
$this->guzzleConfig = [
|
||||
'headers' => ['User-Agent' => config('secure.user_agent')],
|
||||
'verify' => config('secure.certificates')
|
||||
];
|
||||
}
|
||||
|
||||
public function showUpdatePage()
|
||||
|
|
@ -53,7 +92,7 @@ class UpdateController extends Controller
|
|||
);
|
||||
|
||||
if ($detail = $this->getReleaseInfo($info['latest_version'])) {
|
||||
$info = array_merge($info, Arr::only($detail, [
|
||||
$info = array_merge($info, array_only($detail, [
|
||||
'release_note',
|
||||
'release_url',
|
||||
'release_time',
|
||||
|
|
@ -65,24 +104,19 @@ class UpdateController extends Controller
|
|||
}
|
||||
|
||||
if (! $info['new_version_available']) {
|
||||
$info['release_time'] = Arr::get($this->getReleaseInfo($this->currentVersion), 'release_time');
|
||||
$info['release_time'] = array_get($this->getReleaseInfo($this->currentVersion), 'release_time');
|
||||
}
|
||||
}
|
||||
|
||||
$update = Option::form('update', OptionForm::AUTO_DETECT, function($form)
|
||||
{
|
||||
$form->checkbox('check_update', OptionForm::AUTO_DETECT)->label(OptionForm::AUTO_DETECT);
|
||||
$form->text('update_source', OptionForm::AUTO_DETECT)
|
||||
->description(OptionForm::AUTO_DETECT);
|
||||
})->handle()->always(function($form) {
|
||||
try {
|
||||
$response = file_get_contents(option('update_source'));
|
||||
} catch (\Exception $e) {
|
||||
$form->addMessage(trans('admin.update.errors.connection').$e->getMessage(), 'danger');
|
||||
}
|
||||
});
|
||||
$connectivity = true;
|
||||
|
||||
return view('admin.update')->with('info', $info)->with('update', $update);
|
||||
try {
|
||||
$this->guzzle->request('GET', $this->updateSource, $this->guzzleConfig);
|
||||
} catch (Exception $e) {
|
||||
$connectivity = $e->getMessage();
|
||||
}
|
||||
|
||||
return view('admin.update', compact('info', 'connectivity'));
|
||||
}
|
||||
|
||||
public function checkUpdates()
|
||||
|
|
@ -102,17 +136,17 @@ class UpdateController extends Controller
|
|||
|
||||
public function download(Request $request)
|
||||
{
|
||||
$action = $request->input('action');
|
||||
|
||||
if (! $this->newVersionAvailable()) return;
|
||||
if (! $this->newVersionAvailable())
|
||||
return;
|
||||
|
||||
$action = $request->get('action');
|
||||
$release_url = $this->getReleaseInfo($this->latestVersion)['release_url'];
|
||||
$file_size = Utils::getRemoteFileSize($release_url);
|
||||
$tmp_path = session('tmp_path');
|
||||
$tmp_path = Cache::get('tmp_path');
|
||||
|
||||
switch ($action) {
|
||||
case 'prepare-download':
|
||||
|
||||
Cache::forget('download-progress');
|
||||
$update_cache = storage_path('update_cache');
|
||||
|
||||
if (! is_dir($update_cache)) {
|
||||
|
|
@ -121,38 +155,50 @@ class UpdateController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
$tmp_path = $update_cache."/update_".time().".zip";
|
||||
// Set temporary path for the update package
|
||||
$tmp_path = $update_cache.'/update_'.time().'.zip';
|
||||
Cache::put('tmp_path', $tmp_path, 60);
|
||||
Log::info('[UpdateWizard] Prepare to download update package', compact('release_url', 'tmp_path'));
|
||||
|
||||
session(['tmp_path' => $tmp_path]);
|
||||
|
||||
return json(compact('release_url', 'tmp_path', 'file_size'));
|
||||
// We won't get remote file size here since HTTP HEAD method is not always reliable
|
||||
return json(compact('release_url', 'tmp_path'));
|
||||
|
||||
case 'start-download':
|
||||
|
||||
if (! session()->has('tmp_path')) {
|
||||
return "No temp path is set.";
|
||||
if (! $tmp_path) {
|
||||
return 'No temp path available, please try again.';
|
||||
}
|
||||
|
||||
@set_time_limit(0);
|
||||
$GLOBALS['last_downloaded'] = 0;
|
||||
|
||||
Log::info('[UpdateWizard] Start downloading update package');
|
||||
|
||||
try {
|
||||
Utils::download($release_url, $tmp_path);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
File::delete($tmp_path);
|
||||
|
||||
$this->guzzle->request('GET', $release_url, array_merge($this->guzzleConfig, [
|
||||
'sink' => $tmp_path,
|
||||
'progress' => function ($total, $downloaded) {
|
||||
if ($total == 0) return;
|
||||
// Log current progress per 100 KiB
|
||||
if ($total == $downloaded || floor($downloaded / 102400) > floor($GLOBALS['last_downloaded'] / 102400)) {
|
||||
$GLOBALS['last_downloaded'] = $downloaded;
|
||||
Log::info('[UpdateWizard] Download progress (in bytes):', [$total, $downloaded]);
|
||||
Cache::put('download-progress', compact('total', 'downloaded'), 60);
|
||||
}
|
||||
}
|
||||
]));
|
||||
} catch (Exception $e) {
|
||||
@unlink($tmp_path);
|
||||
return response(trans('admin.update.errors.prefix').$e->getMessage());
|
||||
}
|
||||
|
||||
Log::info('[UpdateWizard] Finished downloading update package');
|
||||
|
||||
return json(compact('tmp_path'));
|
||||
|
||||
case 'get-file-size':
|
||||
case 'get-progress':
|
||||
|
||||
if (! session()->has('tmp_path')) {
|
||||
return "No temp path is set.";
|
||||
}
|
||||
|
||||
if (file_exists($tmp_path)) {
|
||||
return json(['size' => filesize($tmp_path)]);
|
||||
}
|
||||
return json((array) Cache::get('download-progress'));
|
||||
|
||||
case 'extract':
|
||||
|
||||
|
|
@ -166,7 +212,7 @@ class UpdateController extends Controller
|
|||
$res = $zip->open($tmp_path);
|
||||
|
||||
if ($res === true) {
|
||||
Log::info("[ZipArchive] Extracting file $tmp_path");
|
||||
Log::info("[UpdateWizard] Extracting file $tmp_path");
|
||||
|
||||
if ($zip->extractTo($extract_dir) === false) {
|
||||
return response(trans('admin.update.errors.prefix').'Cannot unzip file.');
|
||||
|
|
@ -179,29 +225,31 @@ class UpdateController extends Controller
|
|||
|
||||
try {
|
||||
File::copyDirectory("$extract_dir/vendor", base_path('vendor'));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[Extracter] Unable to extract vendors', [$e]);
|
||||
} catch (Exception $e) {
|
||||
report($e);
|
||||
Log::error('[UpdateWizard] Unable to extract vendors');
|
||||
// Skip copying vendor
|
||||
File::deleteDirectory("$extract_dir/vendor");
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
File::copyDirectory($extract_dir, base_path());
|
||||
|
||||
Log::info("[Extracter] Covering files");
|
||||
Log::info('[UpdateWizard] Overwrite with extracted files');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("[Extracter] Error occured when covering files", [$e]);
|
||||
} catch (Exception $e) {
|
||||
report($e);
|
||||
Log::error('[UpdateWizard] Error occured when overwriting files');
|
||||
|
||||
// Response can be returned, while cache will be cleared
|
||||
// @see https://gist.github.com/g-plane/2f88ad582826a78e0a26c33f4319c1e0
|
||||
return response(trans('admin.update.errors.overwrite').$e->getMessage());
|
||||
} finally {
|
||||
File::deleteDirectory(storage_path('update_cache'));
|
||||
Log::info("[Extracter] Cleaning cache");
|
||||
Log::info('[UpdateWizard] Cleaning cache');
|
||||
}
|
||||
|
||||
Log::info('[UpdateWizard] Done');
|
||||
return json(trans('admin.update.complete'), 0);
|
||||
|
||||
default:
|
||||
|
|
@ -218,21 +266,20 @@ class UpdateController extends Controller
|
|||
: $this->updateSource;
|
||||
|
||||
try {
|
||||
$response = file_get_contents($url);
|
||||
} catch (\Exception $e) {
|
||||
$response = $this->guzzle->request('GET', $url, $this->guzzleConfig)->getBody();
|
||||
} catch (Exception $e) {
|
||||
Log::error("[CheckingUpdate] Failed to get update information: ".$e->getMessage());
|
||||
}
|
||||
|
||||
if (isset($response)) {
|
||||
$this->updateInfo = json_decode($response, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->latestVersion = Arr::get($this->updateInfo, 'latest_version', $this->currentVersion);
|
||||
$this->latestVersion = array_get($this->updateInfo, 'latest_version', $this->currentVersion);
|
||||
|
||||
if (! is_null($key)) {
|
||||
return Arr::get($this->updateInfo, $key);
|
||||
return array_get($this->updateInfo, $key);
|
||||
}
|
||||
|
||||
return $this->updateInfo;
|
||||
|
|
@ -240,7 +287,7 @@ class UpdateController extends Controller
|
|||
|
||||
protected function getReleaseInfo($version)
|
||||
{
|
||||
return Arr::get($this->getUpdateInfo('releases'), $version);
|
||||
return array_get($this->getUpdateInfo('releases'), $version);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App;
|
||||
use Mail;
|
||||
use View;
|
||||
use Utils;
|
||||
use Schema;
|
||||
use Session;
|
||||
use App\Models\User;
|
||||
use App\Models\Texture;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -24,6 +26,12 @@ class UserController extends Controller
|
|||
public function __construct(UserRepository $users)
|
||||
{
|
||||
$this->user = $users->get(session('uid'));
|
||||
|
||||
// Do nothing if new columns are not ready
|
||||
if (Schema::hasColumn('users', 'verified') && option('require_verification')) {
|
||||
// Send email verification link to newly registered users
|
||||
$this->user->verification_token || $this->sendVerificationEmail();
|
||||
}
|
||||
}
|
||||
|
||||
public function index()
|
||||
|
|
@ -94,6 +102,49 @@ class UserController extends Controller
|
|||
return $hours > 1 ? round($hours) : $hours;
|
||||
}
|
||||
|
||||
public function sendVerificationEmail()
|
||||
{
|
||||
if (!option('require_verification') || !Schema::hasColumn('users', 'verified')) {
|
||||
return json(trans('user.verification.disabled'), 1);
|
||||
}
|
||||
|
||||
// Rate limit of 60s
|
||||
$remain = 60 + session('last_mail_time', 0) - time();
|
||||
|
||||
if ($remain > 0) {
|
||||
return json(trans('user.verification.frequent-mail'), 1);
|
||||
}
|
||||
|
||||
if ($this->user->verified) {
|
||||
return json(trans('user.verification.verified'), 1);
|
||||
}
|
||||
|
||||
$token = generate_random_token();
|
||||
$this->user->verification_token = $token;
|
||||
$this->user->save();
|
||||
|
||||
$email = $this->user->email;
|
||||
$url = option('site_url')."/auth/verify?uid={$this->user->uid}&token=$token";
|
||||
|
||||
try {
|
||||
Mail::send('mails.email-verification', compact('url'), function ($m) use ($email) {
|
||||
$site_name = option_localized('site_name');
|
||||
|
||||
$m->from(config('mail.username'), $site_name);
|
||||
$m->to($email)->subject(trans('user.verification.mail.title', ['sitename' => $site_name]));
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
// Write the exception to log
|
||||
report($e);
|
||||
|
||||
return json(trans('user.verification.failed', ['msg' => $e->getMessage()]), 2);
|
||||
}
|
||||
|
||||
Session::put('last_mail_time', time());
|
||||
|
||||
return json(trans('user.verification.success'), 0);
|
||||
}
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return view('user.profile')->with('user', $this->user);
|
||||
|
|
@ -160,6 +211,11 @@ class UserController extends Controller
|
|||
return json(trans('user.profile.email.wrong-password'), 1);
|
||||
|
||||
if ($this->user->setEmail($request->input('new_email'))) {
|
||||
// Set account status to unverified
|
||||
$this->user->verified = false;
|
||||
$this->user->verification_token = '';
|
||||
$this->user->save();
|
||||
|
||||
event(new UserProfileUpdated($action, $this->user));
|
||||
|
||||
return json(trans('user.profile.email.success'), 0)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ class Kernel extends HttpKernel
|
|||
*/
|
||||
protected $middleware = [
|
||||
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
|
||||
\App\Http\Middleware\DetectLanguagePrefer::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -31,6 +30,7 @@ class Kernel extends HttpKernel
|
|||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\AfterSessionBooted::class,
|
||||
\App\Http\Middleware\DetectLanguagePrefer::class,
|
||||
],
|
||||
|
||||
'static' => [],
|
||||
|
|
@ -44,10 +44,13 @@ class Kernel extends HttpKernel
|
|||
* @var array
|
||||
*/
|
||||
protected $routeMiddleware = [
|
||||
'auth' => \App\Http\Middleware\CheckAuthenticated::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'admin' => \App\Http\Middleware\CheckAdministrator::class,
|
||||
'player' => \App\Http\Middleware\CheckPlayerExist::class,
|
||||
'setup' => \App\Http\Middleware\CheckInstallation::class,
|
||||
'csrf' => \App\Http\Middleware\VerifyCsrfToken::class,
|
||||
'auth' => \App\Http\Middleware\CheckAuthenticated::class,
|
||||
'verified' => \App\Http\Middleware\CheckUserVerified::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'admin' => \App\Http\Middleware\CheckAdministrator::class,
|
||||
'super-admin' => \App\Http\Middleware\CheckSuperAdmin::class,
|
||||
'player' => \App\Http\Middleware\CheckPlayerExist::class,
|
||||
'setup' => \App\Http\Middleware\CheckInstallation::class,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,15 @@
|
|||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CheckAdministrator
|
||||
{
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
$result = (new CheckAuthenticated)->handle($request, $next, true);
|
||||
|
||||
if ($result instanceof \Illuminate\Http\RedirectResponse) {
|
||||
if ($result instanceof Response) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,12 +38,10 @@ class CheckPlayerExist
|
|||
if (! Player::where('player_name', $player_name)->get()->isEmpty())
|
||||
return $next($request);
|
||||
|
||||
if (option('return_200_when_notfound')) {
|
||||
return json([
|
||||
'player_name' => $player_name,
|
||||
'errno' => 404,
|
||||
'msg' => 'Player Not Found.'
|
||||
])->header('Cache-Control', 'public, max-age='.option('cache_expire_time'));
|
||||
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'));
|
||||
}
|
||||
|
|
|
|||
23
app/Http/Middleware/CheckSuperAdmin.php
Normal file
23
app/Http/Middleware/CheckSuperAdmin.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CheckSuperAdmin
|
||||
{
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
$result = (new CheckAuthenticated)->handle($request, $next, true);
|
||||
|
||||
if ($result instanceof Response) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (! $result->isSuperAdmin()) {
|
||||
abort(403, trans('auth.check.super-admin'));
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
21
app/Http/Middleware/CheckUserVerified.php
Normal file
21
app/Http/Middleware/CheckUserVerified.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
class CheckUserVerified
|
||||
{
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
$result = (new CheckAuthenticated)->handle($request, $next, true);
|
||||
|
||||
if ($result instanceof Response) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (option('require_verification') && !$result->verified) {
|
||||
abort(403, trans('auth.check.verified'));
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
17
app/Http/Middleware/VerifyCsrfToken.php
Normal file
17
app/Http/Middleware/VerifyCsrfToken.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
|
||||
|
||||
class VerifyCsrfToken extends BaseVerifier
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
namespace App\Models;
|
||||
|
||||
use Event;
|
||||
use Utils;
|
||||
use Response;
|
||||
use App\Models\User;
|
||||
use App\Events\GetPlayerJson;
|
||||
|
|
@ -98,7 +97,7 @@ class Player extends Model
|
|||
}
|
||||
}
|
||||
|
||||
$this->last_modified = Utils::getTimeFormatted();
|
||||
$this->last_modified = get_datetime_string();
|
||||
|
||||
$this->save();
|
||||
|
||||
|
|
@ -159,7 +158,7 @@ class Player extends Model
|
|||
{
|
||||
$this->update([
|
||||
'preference' => $type,
|
||||
'last_modified' => Utils::getTimeFormatted()
|
||||
'last_modified' => get_datetime_string()
|
||||
]);
|
||||
|
||||
event(new PlayerProfileUpdated($this));
|
||||
|
|
@ -187,7 +186,7 @@ class Player extends Model
|
|||
{
|
||||
$this->update([
|
||||
'player_name' => $newName,
|
||||
'last_modified' => Utils::getTimeFormatted()
|
||||
'last_modified' => get_datetime_string()
|
||||
]);
|
||||
|
||||
$this->player_name = $newName;
|
||||
|
|
@ -249,7 +248,7 @@ class Player extends Model
|
|||
$sec_model = ($model == 'default') ? 'slim' : 'default';
|
||||
|
||||
if ($api_type == self::USM_API) {
|
||||
$json['last_update'] = $this->getLastModified();
|
||||
$json['last_update'] = strtotime($this->last_modified);
|
||||
$json['model_preference'] = [$model, $sec_model];
|
||||
}
|
||||
|
||||
|
|
@ -272,17 +271,7 @@ class Player extends Model
|
|||
public function updateLastModified()
|
||||
{
|
||||
// @see http://stackoverflow.com/questions/2215354/php-date-format-when-inserting-into-datetime-in-mysql
|
||||
$this->update(['last_modified' => Utils::getTimeFormatted()]);
|
||||
$this->update(['last_modified' => get_datetime_string()]);
|
||||
return event(new PlayerProfileUpdated($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time of last modified.
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
public function getLastModified()
|
||||
{
|
||||
return strtotime($this['last_modified']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
namespace App\Models;
|
||||
|
||||
use DB;
|
||||
use Utils;
|
||||
use Carbon\Carbon;
|
||||
use App\Events\EncryptUserPassword;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
|
@ -47,6 +46,7 @@ class User extends Model
|
|||
'score' => 'integer',
|
||||
'avatar' => 'integer',
|
||||
'permission' => 'integer',
|
||||
'verified' => 'bool',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -66,6 +66,16 @@ class User extends Model
|
|||
return ($this->permission >= static::ADMIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is super admin.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSuperAdmin()
|
||||
{
|
||||
return ($this->permission == static::SUPER_ADMIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get closet instance.
|
||||
*
|
||||
|
|
@ -300,7 +310,7 @@ class User extends Model
|
|||
$acquiredScore = rand($scoreLimits[0], $scoreLimits[1]);
|
||||
|
||||
$this->setScore($acquiredScore, 'plus');
|
||||
$this->last_sign_at = Utils::getTimeFormatted();
|
||||
$this->last_sign_at = get_datetime_string();
|
||||
$this->save();
|
||||
|
||||
return $acquiredScore;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,15 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use View;
|
||||
use Event;
|
||||
use Utils;
|
||||
use Parsedown;
|
||||
use App\Events;
|
||||
use ReflectionException;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
use App\Services\Repositories\UserRepository;
|
||||
use App\Services\Repositories\OptionRepository;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -16,21 +21,16 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot()
|
||||
{
|
||||
// Replace HTTP_HOST with site url setted in options to prevent CDN source problems
|
||||
if (! option('auto_detect_asset_url')) {
|
||||
$rootUrl = option('site_url');
|
||||
// Support *.tpl extension name
|
||||
View::addExtension('tpl', 'blade');
|
||||
// Make the priority of *.blade.php higher than *.tpl
|
||||
View::addExtension('blade.php', 'blade');
|
||||
|
||||
if ($this->app['url']->isValidUrl($rootUrl)) {
|
||||
$this->app['url']->forceRootUrl($rootUrl);
|
||||
}
|
||||
}
|
||||
// Control the URL generated by url() function
|
||||
$this->configureUrlGenerator();
|
||||
|
||||
if (option('force_ssl') || Utils::isRequestSecure()) {
|
||||
$this->app['url']->forceSchema('https');
|
||||
}
|
||||
|
||||
Event::listen(Events\RenderingHeader::class, function($event) {
|
||||
// Provide some application information for javascript
|
||||
// Expose some app information to front-end
|
||||
Event::listen(Events\RenderingHeader::class, function ($event) {
|
||||
$blessing = array_merge(array_except(config('app'), ['key', 'providers', 'aliases', 'cipher', 'log', 'url']), [
|
||||
'base_url' => url('/'),
|
||||
'site_name' => option_localized('site_name')
|
||||
|
|
@ -38,6 +38,34 @@ class AppServiceProvider extends ServiceProvider
|
|||
|
||||
$event->addContent('<script>var blessing = '.json_encode($blessing).';</script>');
|
||||
});
|
||||
|
||||
try {
|
||||
$this->app->make('cipher');
|
||||
} catch (ReflectionException $e) {
|
||||
throw new PrettyPageException(trans('errors.cipher.unsupported', ['cipher' => config('secure.cipher')]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the \Illuminate\Routing\UrlGenerator.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function configureUrlGenerator()
|
||||
{
|
||||
if (! option('auto_detect_asset_url')) {
|
||||
$rootUrl = option('site_url');
|
||||
|
||||
// Replace HTTP_HOST with site_url set in options,
|
||||
// to prevent CDN source problems.
|
||||
if ($this->app['url']->isValidUrl($rootUrl)) {
|
||||
$this->app['url']->forceRootUrl($rootUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (option('force_ssl') || is_request_secure()) {
|
||||
$this->app['url']->forceSchema('https');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -47,19 +75,9 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function register()
|
||||
{
|
||||
// Register default cipher
|
||||
$className = "App\Services\Cipher\\".config('secure.cipher');
|
||||
|
||||
if (class_exists($className)) {
|
||||
$this->app->singleton('cipher', $className);
|
||||
} else {
|
||||
die_with_utf8_encoding(sprintf(
|
||||
'[Error] Unsupported encryption method: < %1$s >, please check your .env configuration <br>'.
|
||||
'[错误] 不支持的密码加密方式 < %1$s >,请检查你的 .env 配置文件'
|
||||
, config('secure.cipher')));
|
||||
}
|
||||
|
||||
$this->app->singleton('users', \App\Services\Repositories\UserRepository::class);
|
||||
$this->app->singleton('parsedown', \Parsedown::class);
|
||||
$this->app->singleton('cipher', 'App\Services\Cipher\\'.config('secure.cipher'));
|
||||
$this->app->singleton('parsedown', Parsedown::class);
|
||||
$this->app->singleton('users', UserRepository::class);
|
||||
$this->app->singleton('options', OptionRepository::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
41
app/Providers/LogServiceProvider.php
Normal file
41
app/Providers/LogServiceProvider.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Log;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class LogServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
Log::getMonolog()->popHandler();
|
||||
Log::useFiles($this->getLogPath());
|
||||
|
||||
if (! config('app.debug')) {
|
||||
@unlink(storage_path('logs/laravel.log'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
protected static function getLogPath()
|
||||
{
|
||||
$mask = substr(md5(implode(',', array_values(get_db_config()))), 0, 16);
|
||||
|
||||
return storage_path("logs/bs-$mask.log");
|
||||
}
|
||||
}
|
||||
|
|
@ -16,41 +16,41 @@ class ResponseMacroServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot()
|
||||
{
|
||||
Response::macro('png', function ($src = "", $status = 200, $header = []) {
|
||||
Response::macro('png', function ($src = '', $status = 200, $header = []) {
|
||||
// Handle fucking cache control
|
||||
$last_modified = Arr::pull($header, 'Last-Modified', time());
|
||||
$if_modified_since = strtotime(request()->headers->get('If-Modified-Since'));
|
||||
$if_none_match = strtotime(request()->headers->get('If-None-Match'));
|
||||
$etag = md5($src);
|
||||
|
||||
// Checking if the client is validating his cache and if it is current.
|
||||
if ((strtotime(Arr::get($_SERVER, 'If-Modified-Since')) == $last_modified) ||
|
||||
trim(Arr::get($_SERVER, 'HTTP_IF_NONE_MATCH')) == $etag
|
||||
) {
|
||||
// Client's cache IS current, so we just respond '304 Not Modified'.
|
||||
// Return `304 Not Modified` if given `If-Modified-Since` header
|
||||
// is newer than our `Last-Modified` time or the `Etag` matches.
|
||||
if ($if_modified_since >= $last_modified || $if_none_match == $etag) {
|
||||
$src = '';
|
||||
$status = 304;
|
||||
$src = "";
|
||||
}
|
||||
|
||||
return Response::stream(function() use ($src, $status) {
|
||||
echo $src;
|
||||
}, $status, array_merge([
|
||||
return Response::make($src, $status, array_merge([
|
||||
'Content-type' => 'image/png',
|
||||
'Last-Modified' => gmdate('D, d M Y H:i:s', $last_modified).' GMT',
|
||||
'Cache-Control' => 'public, max-age='.option('cache_expire_time'), // 365 days
|
||||
'Expires' => gmdate('D, d M Y H:i:s', $last_modified + option('cache_expire_time')).' GMT',
|
||||
'Last-Modified' => format_http_date($last_modified),
|
||||
'Cache-Control' => 'public, max-age='.option('cache_expire_time'),
|
||||
'Etag' => $etag
|
||||
], $header));
|
||||
});
|
||||
|
||||
Response::macro('rawJson', function ($src = "", $status = 200, $header = []) {
|
||||
$last_modified = Arr::get($header, 'Last-Modified', time());
|
||||
Response::macro('jsonProfile', function ($src = '', $status = 200, $header = []) {
|
||||
$last_modified = Arr::pull($header, 'Last-Modified', time());
|
||||
$if_modified_since = strtotime(request()->headers->get('If-Modified-Since'));
|
||||
|
||||
if (strtotime(Arr::get($_SERVER, 'If-Modified-Since')) >= $last_modified) {
|
||||
if ($if_modified_since && $if_modified_since >= $last_modified) {
|
||||
$src = '';
|
||||
$status = 304;
|
||||
$src = "";
|
||||
}
|
||||
|
||||
return Response::make($src, $status, array_merge([
|
||||
'Content-type' => 'application/json',
|
||||
'Cache-Control' => 'public, max-age='.option('cache_expire_time') // 365 days
|
||||
'Cache-Control' => 'public, max-age='.option('cache_expire_time'),
|
||||
'Last-Modified' => format_http_date($last_modified),
|
||||
], $header));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||
protected function mapWebRoutes(Router $router)
|
||||
{
|
||||
$router->group([
|
||||
'middleware' => ['web', CheckSessionUserValid::class],
|
||||
'middleware' => ['web', CheckSessionUserValid::class, 'csrf'],
|
||||
'namespace' => $this->namespace,
|
||||
], function ($router) {
|
||||
require base_path('routes/web.php');
|
||||
|
|
|
|||
|
|
@ -3,16 +3,15 @@
|
|||
namespace App\Providers;
|
||||
|
||||
use DB;
|
||||
use View;
|
||||
use Utils;
|
||||
use Schema;
|
||||
use Artisan;
|
||||
use Illuminate\Http\Request;
|
||||
use Composer\Semver\Comparator;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
use App\Http\Controllers\SetupController;
|
||||
use App\Services\Repositories\OptionRepository;
|
||||
|
||||
class BootServiceProvider extends ServiceProvider
|
||||
class RuntimeCheckServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
|
|
@ -27,10 +26,13 @@ class BootServiceProvider extends ServiceProvider
|
|||
$this->checkFilePermissions();
|
||||
$this->checkDatabaseConnection();
|
||||
|
||||
// Skip the installation check when setup or under CLI
|
||||
if (! $request->is('setup*') && PHP_SAPI != "cli") {
|
||||
$this->checkInstallation();
|
||||
// Skip the installation check on setup wizard or under CLI
|
||||
if ($request->is('setup*') || $this->app->runningInConsole()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Redirect to setup wizard if not installed
|
||||
$this->checkInstallation();
|
||||
}
|
||||
|
||||
protected function checkFilePermissions()
|
||||
|
|
@ -40,6 +42,10 @@ class BootServiceProvider extends ServiceProvider
|
|||
throw new PrettyPageException(trans('setup.file.no-dot-env'), -1);
|
||||
}
|
||||
|
||||
if (! is_readable(app()->environmentFile())) {
|
||||
throw new PrettyPageException(trans('setup.file.dot-env-no-read-permission'), -1);
|
||||
}
|
||||
|
||||
// Check permissions of storage path
|
||||
if (! is_writable(storage_path())) {
|
||||
throw new PrettyPageException(trans('setup.permissions.storage'), -1);
|
||||
|
|
@ -78,11 +84,20 @@ class BootServiceProvider extends ServiceProvider
|
|||
{
|
||||
// Redirect to setup wizard
|
||||
if (! SetupController::checkTablesExist()) {
|
||||
return redirect('/setup')->send();
|
||||
redirect('/setup')->send();
|
||||
exit;
|
||||
}
|
||||
|
||||
if (Comparator::greaterThan(config('app.version'), option('version'))) {
|
||||
return redirect('/setup/update')->send();
|
||||
redirect('/setup/update')->send();
|
||||
exit;
|
||||
}
|
||||
|
||||
if (! SetupController::checkNewColumnsExist()) {
|
||||
// Disable the email verification feature temporarily
|
||||
option(['require_verification' => false]);
|
||||
// Try to prepare the new columns
|
||||
Artisan::call('migrate', ['--force' => true]);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -95,8 +110,6 @@ class BootServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function register()
|
||||
{
|
||||
View::addExtension('tpl', 'blade');
|
||||
|
||||
$this->app->singleton('options', OptionRepository::class);
|
||||
//
|
||||
}
|
||||
}
|
||||
16
app/Providers/TranslationServiceProvider.php
Normal file
16
app/Providers/TranslationServiceProvider.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\TranslationLoader;
|
||||
use Illuminate\Translation\TranslationServiceProvider as IlluminateTranslationServiceProvider;
|
||||
|
||||
class TranslationServiceProvider extends IlluminateTranslationServiceProvider
|
||||
{
|
||||
protected function registerLoader()
|
||||
{
|
||||
$this->app->singleton('translation.loader', function ($app) {
|
||||
return new TranslationLoader($app['files'], $app['path.lang']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ class ValidatorExtendServiceProvider extends ServiceProvider
|
|||
* @param $v validator
|
||||
*/
|
||||
Validator::extend('no_special_chars', function ($a, $value, $p, $v) {
|
||||
return $value === addslashes(trim($value));
|
||||
return $value === e(addslashes(trim($value)));
|
||||
});
|
||||
|
||||
Validator::extend('player_name', function ($a, $value, $p, $v) {
|
||||
|
|
@ -46,6 +46,10 @@ class ValidatorExtendServiceProvider extends ServiceProvider
|
|||
return preg_match($regexp, $value);
|
||||
});
|
||||
|
||||
Validator::extend('texture_name_regexp', function ($a, $value, $p, $v) {
|
||||
return preg_match(option('texture_name_regexp'), $value);
|
||||
});
|
||||
|
||||
Validator::extend('preference', function ($a, $value, $p, $v) {
|
||||
return preg_match('/^(default|slim)$/', $value);
|
||||
});
|
||||
|
|
@ -65,7 +69,7 @@ class ValidatorExtendServiceProvider extends ServiceProvider
|
|||
protected function registerExpiredRules()
|
||||
{
|
||||
Validator::extend('nickname', function ($a, $value, $p, $v) {
|
||||
return $value === addslashes(trim($value));
|
||||
return $value === e(addslashes(trim($value)));
|
||||
});
|
||||
|
||||
Validator::extend('playername', function($a, $value, $p, $v) {
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Cipher;
|
||||
|
||||
class CrazyCrypt1 extends BaseCipher
|
||||
{
|
||||
/**
|
||||
* Once SHA512 hash
|
||||
*/
|
||||
public function hash($value, $salt = "")
|
||||
{
|
||||
// fucking CrazyCrypt1 uses username as salt
|
||||
$username = $salt;
|
||||
|
||||
$text = "ÜÄaeut//&/=I " . $value . "7421€547" . $username . "__+IÄIH§%NK " . $value;
|
||||
$t1 = unpack("H*", $text);
|
||||
$t2 = substr($t1[1], 0, mb_strlen($text, 'UTF-8')*2);
|
||||
$t3 = pack("H*", $t2);
|
||||
|
||||
return hash('sha512', $t3);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,10 +23,9 @@ class Hook
|
|||
*/
|
||||
public static function addMenuItem($category, $position, array $menu)
|
||||
{
|
||||
$class = $category == "user" ? Events\ConfigureUserMenu::class : Events\ConfigureAdminMenu::class;
|
||||
$class = $category == 'user' ? Events\ConfigureUserMenu::class : Events\ConfigureAdminMenu::class;
|
||||
|
||||
Event::listen($class, function ($event) use ($menu, $position, $category)
|
||||
{
|
||||
Event::listen($class, function ($event) use ($menu, $position, $category) {
|
||||
$new = [];
|
||||
|
||||
$offset = 0;
|
||||
|
|
@ -40,6 +39,10 @@ class Hook
|
|||
$offset++;
|
||||
}
|
||||
|
||||
if ($position >= $offset) {
|
||||
$new[] = $menu;
|
||||
}
|
||||
|
||||
$event->menu[$category] = $new;
|
||||
});
|
||||
}
|
||||
|
|
@ -53,29 +56,36 @@ class Hook
|
|||
*/
|
||||
public static function addRoute(Closure $callback)
|
||||
{
|
||||
Event::listen(Events\ConfigureRoutes::class, function($event) use ($callback)
|
||||
{
|
||||
Event::listen(Events\ConfigureRoutes::class, function ($event) use ($callback) {
|
||||
return call_user_func($callback, $event->router);
|
||||
});
|
||||
}
|
||||
|
||||
public static function registerPluginTransScripts($id)
|
||||
public static function registerPluginTransScripts($id, $pages = ['*'], $priority = 999)
|
||||
{
|
||||
Event::listen(Events\RenderingFooter::class, function($event) use ($id)
|
||||
{
|
||||
$path = app('plugins')->getPlugin($id)->getPath().'/';
|
||||
$script = 'lang/'.config('app.locale').'/locale.js';
|
||||
Event::listen(Events\RenderingFooter::class, function ($event) use ($id, $pages) {
|
||||
foreach ($pages as $pattern) {
|
||||
if (! app('request')->is($pattern))
|
||||
continue;
|
||||
|
||||
if (file_exists($path.$script)) {
|
||||
$event->addContent('<script src="'.plugin_assets($id, $script).'"></script>');
|
||||
// We will determine current locale in the event callback,
|
||||
// otherwise the locale is not properly detected.
|
||||
$basepath = plugin($id)->getPath().'/';
|
||||
$relative = 'lang/'.config('app.locale').'/locale.js';
|
||||
|
||||
if (file_exists($basepath.$relative)) {
|
||||
$event->addContent('<script src="'.plugin_assets($id, $relative).'"></script>');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}, 999);
|
||||
}, $priority);
|
||||
}
|
||||
|
||||
public static function addStyleFileToPage($urls, $pages = ['*'], $priority = 1)
|
||||
{
|
||||
Event::listen(Events\RenderingHeader::class, function($event) use ($urls, $pages)
|
||||
{
|
||||
Event::listen(Events\RenderingHeader::class, function ($event) use ($urls, $pages) {
|
||||
|
||||
foreach ($pages as $pattern) {
|
||||
if (! app('request')->is($pattern))
|
||||
continue;
|
||||
|
|
@ -92,8 +102,8 @@ class Hook
|
|||
|
||||
public static function addScriptFileToPage($urls, $pages = ['*'], $priority = 1)
|
||||
{
|
||||
Event::listen(Events\RenderingFooter::class, function($event) use ($urls, $pages)
|
||||
{
|
||||
Event::listen(Events\RenderingFooter::class, function ($event) use ($urls, $pages) {
|
||||
|
||||
foreach ($pages as $pattern) {
|
||||
if (! app('request')->is($pattern))
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -277,20 +277,24 @@ class PluginManager
|
|||
/**
|
||||
* Get the unsatisfied requirements of plugin.
|
||||
*
|
||||
* @param string|Plugin $plugin
|
||||
* @param string|Plugin|array $plugin
|
||||
* @return array
|
||||
*/
|
||||
public function getUnsatisfiedRequirements($plugin)
|
||||
{
|
||||
if (! $plugin instanceof Plugin) {
|
||||
$plugin = $this->getPlugin($plugin);
|
||||
}
|
||||
if (is_array($plugin)) {
|
||||
$requirements = $plugin;
|
||||
} else {
|
||||
if (! $plugin instanceof Plugin) {
|
||||
$plugin = $this->getPlugin($plugin);
|
||||
}
|
||||
|
||||
if (! $plugin) {
|
||||
throw new \InvalidArgumentException('Plugin with given name does not exist.');
|
||||
}
|
||||
if (! $plugin) {
|
||||
throw new \InvalidArgumentException('Plugin with given name does not exist.');
|
||||
}
|
||||
|
||||
$requirements = $plugin->getRequirements();
|
||||
$requirements = $plugin->getRequirements();
|
||||
}
|
||||
|
||||
$unsatisfied = [];
|
||||
|
||||
|
|
@ -334,7 +338,7 @@ class PluginManager
|
|||
/**
|
||||
* Whether the plugin's requirements are satisfied.
|
||||
*
|
||||
* @param string|Plugin $plugin
|
||||
* @param string|Plugin|array $plugin
|
||||
* @return bool
|
||||
*/
|
||||
public function isRequirementsSatisfied($plugin)
|
||||
|
|
@ -347,7 +351,7 @@ class PluginManager
|
|||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getPluginsDir()
|
||||
public function getPluginsDir()
|
||||
{
|
||||
return config('plugins.directory') ?: base_path('plugins');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Services\Repositories;
|
||||
|
||||
use DB;
|
||||
use PDOException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Database\QueryException;
|
||||
|
||||
|
|
@ -103,6 +104,8 @@ class OptionRepository extends Repository
|
|||
}
|
||||
} catch (QueryException $e) {
|
||||
return;
|
||||
} catch (PDOException $e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +135,8 @@ class OptionRepository extends Repository
|
|||
$this->itemsModified = [];
|
||||
} catch (QueryException $e) {
|
||||
return;
|
||||
} catch (PDOException $e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
41
app/Services/TranslationLoader.php
Normal file
41
app/Services/TranslationLoader.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Devitek\Core\Translation\YamlFileLoader;
|
||||
|
||||
class TranslationLoader extends YamlFileLoader
|
||||
{
|
||||
/**
|
||||
* Load the messages for the given locale.
|
||||
*
|
||||
* @param string $locale
|
||||
* @param string $group
|
||||
* @param string $namespace
|
||||
* @return array
|
||||
*/
|
||||
public function load($locale, $group, $namespace = null)
|
||||
{
|
||||
if (is_null($namespace) || $namespace == '*') {
|
||||
// Overrides original translations with custom ones
|
||||
return array_replace_recursive(
|
||||
$this->loadPath($this->path, $locale, $group),
|
||||
$this->loadPathOverrides($locale, $group)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->loadNamespaced($locale, $group, $namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load custom messages from /resources/lang/overrides path.
|
||||
*
|
||||
* @param string $locale
|
||||
* @param string $group
|
||||
* @return array
|
||||
*/
|
||||
protected function loadPathOverrides($locale, $group)
|
||||
{
|
||||
return $this->loadPath("$this->path/overrides", $locale, $group);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,6 @@
|
|||
namespace App\Services;
|
||||
|
||||
use Log;
|
||||
use Storage;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
|
||||
class Utils
|
||||
{
|
||||
|
|
@ -16,21 +12,12 @@ class Utils
|
|||
* This method is defined because Symfony's Request::getClientIp() needs "setTrustedProxies()"
|
||||
* which sucks when load balancer is enabled.
|
||||
*
|
||||
* @deprecated Use the helper function instead.
|
||||
* @return string
|
||||
*/
|
||||
public static function getClientIp()
|
||||
{
|
||||
if (option('ip_get_method') == "0") {
|
||||
// Fallback to REMOTE_ADDR
|
||||
$ip = array_get(
|
||||
$_SERVER, 'HTTP_X_FORWARDED_FOR',
|
||||
array_get($_SERVER, 'HTTP_CLIENT_IP', $_SERVER['REMOTE_ADDR'])
|
||||
);
|
||||
} else {
|
||||
$ip = array_get($_SERVER, 'REMOTE_ADDR');
|
||||
}
|
||||
|
||||
return $ip;
|
||||
return get_client_ip();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -40,125 +27,37 @@ class Utils
|
|||
* This method is defined because Symfony's Request::isSecure() needs "setTrustedProxies()"
|
||||
* which sucks when load balancer is enabled.
|
||||
*
|
||||
* @deprecated Use the helper function instead.
|
||||
* @return bool
|
||||
*/
|
||||
public static function isRequestSecure()
|
||||
{
|
||||
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
|
||||
return true;
|
||||
|
||||
if (! empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
|
||||
return true;
|
||||
|
||||
if (! empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on')
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function download($url, $path)
|
||||
{
|
||||
@set_time_limit(0);
|
||||
|
||||
touch($path);
|
||||
|
||||
Log::info("[File Downloader] Download started, source: $url");
|
||||
Log::info("[File Downloader] ======================================");
|
||||
|
||||
$context = stream_context_create(['http' => [
|
||||
'method' => 'GET',
|
||||
'header' => 'User-Agent: '.menv('USER_AGENT', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36')
|
||||
]]);
|
||||
|
||||
if ($fp = fopen($url, 'rb', false, $context)) {
|
||||
|
||||
if (! $download_fp = fopen($path, 'wb')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (! feof($fp)) {
|
||||
|
||||
if (! file_exists($path)) {
|
||||
// Cancel downloading if destination is no longer available
|
||||
fclose($download_fp);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Log::info('[File Downloader] 1024 bytes wrote');
|
||||
fwrite($download_fp, fread($fp, 1024 * 8 ), 1024 * 8);
|
||||
}
|
||||
|
||||
fclose($download_fp);
|
||||
fclose($fp);
|
||||
|
||||
Log::info("[File Downloader] Finished downloading, data stored to: $path");
|
||||
Log::info("[File Downloader] ===========================================");
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getRemoteFileSize($url)
|
||||
{
|
||||
$regex = '/^Content-Length: *+\K\d++$/im';
|
||||
|
||||
if (! $fp = @fopen($url, 'rb')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($http_response_header) &&
|
||||
preg_match($regex, implode("\n", $http_response_header), $matches)
|
||||
) {
|
||||
return (int)$matches[0];
|
||||
}
|
||||
|
||||
return strlen(stream_get_contents($fp));
|
||||
return is_request_secure();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date time string in "Y-m-d H:i:s" format.
|
||||
*
|
||||
* @deprecated Use the helper function instead.
|
||||
* @param integer $timestamp
|
||||
* @return string
|
||||
*/
|
||||
public static function getTimeFormatted($timestamp = 0)
|
||||
{
|
||||
return ($timestamp == 0) ? Carbon::now()->toDateTimeString() : Carbon::createFromTimestamp($timestamp)->toDateTimeString();
|
||||
return get_datetime_string($timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace content of string according to given rules.
|
||||
*
|
||||
* @deprecated Use the helper function instead.
|
||||
* @param string $str
|
||||
* @param array $rules
|
||||
* @return string
|
||||
*/
|
||||
public static function getStringReplaced($str, $rules)
|
||||
{
|
||||
foreach ($rules as $search => $replace) {
|
||||
$str = str_replace($search, $replace, $str);
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert error number of uploading files to human-readable text.
|
||||
*
|
||||
* @param int $errno
|
||||
* @return string
|
||||
*/
|
||||
public static function convertUploadFileError($errno = 0)
|
||||
{
|
||||
$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.',
|
||||
];
|
||||
|
||||
return $phpFileUploadErrors[$errno];
|
||||
return get_string_replaced($str, $rules);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
157
app/helpers.php
157
app/helpers.php
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Arr;
|
||||
|
|
@ -239,7 +240,7 @@ if (! function_exists('bs_copyright')) {
|
|||
|
||||
function bs_copyright($prefer = null)
|
||||
{
|
||||
$prefer = is_null($prefer) ? Option::get('copyright_prefer', 0, false) : $prefer;
|
||||
$prefer = is_null($prefer) ? option_localized('copyright_prefer', 0) : $prefer;
|
||||
|
||||
$base64CopyrightText = [
|
||||
'UG93ZXJlZCB3aXRoIOKdpCBieSA8YSBocmVmPSJodHRwczovL2dpdGh1Yi5jb20vcHJpbnRlbXB3L2JsZXNzaW5nLXNraW4tc2VydmVyIj5CbGVzc2luZyBTa2luIFNlcnZlcjwvYT4u',
|
||||
|
|
@ -257,7 +258,7 @@ if (! function_exists('bs_custom_copyright')) {
|
|||
|
||||
function bs_custom_copyright()
|
||||
{
|
||||
return Utils::getStringReplaced(option_localized('copyright_text'), [
|
||||
return get_string_replaced(option_localized('copyright_text'), [
|
||||
'{site_name}' => option_localized('site_name'),
|
||||
'{site_url}' => option('site_url')
|
||||
]);
|
||||
|
|
@ -493,3 +494,155 @@ if (! function_exists('get_db_config')) {
|
|||
return config("database.connections.$type");
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('report')) {
|
||||
/**
|
||||
* Report an exception.
|
||||
*
|
||||
* @param \Exception $exception
|
||||
* @return void
|
||||
*/
|
||||
function report($exception)
|
||||
{
|
||||
app(Illuminate\Contracts\Debug\ExceptionHandler::class)->report($exception);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('can_moderate_texture')) {
|
||||
|
||||
function can_moderate_texture($user, $texture) {
|
||||
if (! $user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only uploader and admins can moderate textures
|
||||
return ($texture->uploader == $user->uid || $user->isAdmin());
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('generate_random_token')) {
|
||||
|
||||
function generate_random_token($key = null) {
|
||||
|
||||
if (is_null($key)) {
|
||||
$key = config('app.key');
|
||||
$key = starts_with($key, 'base64:') ? base64_decode(substr($key, 7)) : $key;
|
||||
}
|
||||
|
||||
return hash_hmac('sha256', str_random(40), $key);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('format_http_date')) {
|
||||
/**
|
||||
* Format a UNIX timestamp to string for HTTP headers.
|
||||
*
|
||||
* e.g. Wed, 21 Oct 2015 07:28:00 GMT
|
||||
*
|
||||
* @param int $timestamp
|
||||
* @return string
|
||||
*/
|
||||
function format_http_date($timestamp) {
|
||||
return Carbon::createFromTimestampUTC($timestamp)->format('D, d M Y H:i:s \G\M\T');
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_datetime_string')) {
|
||||
/**
|
||||
* Get date time string in "Y-m-d H:i:s" format.
|
||||
*
|
||||
* @param integer $timestamp
|
||||
* @return string
|
||||
*/
|
||||
function get_datetime_string($timestamp = 0) {
|
||||
return $timestamp == 0 ? Carbon::now()->toDateTimeString() : Carbon::createFromTimestamp($timestamp)->toDateTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_client_ip')) {
|
||||
/**
|
||||
* Return the client IP address.
|
||||
*
|
||||
* We define this function because Symfony's "Request::getClientIp()" method
|
||||
* needs "setTrustedProxies()", which sucks when load balancer is enabled.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function get_client_ip() {
|
||||
if (option('ip_get_method') == "0") {
|
||||
// Use `HTTP_X_FORWARDED_FOR` if available first
|
||||
$ip = array_get(
|
||||
$_SERVER,
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
// Fallback to `HTTP_CLIENT_IP`
|
||||
array_get(
|
||||
$_SERVER,
|
||||
'HTTP_CLIENT_IP',
|
||||
// Fallback to `REMOTE_ADDR`
|
||||
array_get($_SERVER, 'REMOTE_ADDR')
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$ip = array_get($_SERVER, 'REMOTE_ADDR');
|
||||
}
|
||||
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_string_replaced')) {
|
||||
/**
|
||||
* Replace content of string according to given rules.
|
||||
*
|
||||
* @param string $str
|
||||
* @param array $rules
|
||||
* @return string
|
||||
*/
|
||||
function get_string_replaced($str, $rules)
|
||||
{
|
||||
foreach ($rules as $search => $replace) {
|
||||
$str = str_replace($search, $replace, $str);
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('is_request_secure')) {
|
||||
/**
|
||||
* Check whether the request is secure or not.
|
||||
* True is always returned when "X-Forwarded-Proto" header is set.
|
||||
*
|
||||
* We define this function because Symfony's "Request::isSecure()" method
|
||||
* needs "setTrustedProxies()" which sucks when load balancer is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function is_request_secure()
|
||||
{
|
||||
if (array_get($_SERVER, 'HTTPS') == 'on')
|
||||
return true;
|
||||
|
||||
if (array_get($_SERVER, 'HTTP_X_FORWARDED_PROTO') == 'https')
|
||||
return true;
|
||||
|
||||
if (array_get($_SERVER, 'HTTP_X_FORWARDED_SSL') == 'on')
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('nl2p')) {
|
||||
/**
|
||||
* Wrap blocks of text (delimited by \n) in p tags (similar to nl2br).
|
||||
*
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
function nl2p($text) {
|
||||
$parts = explode("\n", $text);
|
||||
$result = '<p>'.implode('</p><p>', $parts).'</p>';
|
||||
// Remove empty paragraphs
|
||||
return str_replace('<p></p>', '', $result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,16 +13,17 @@
|
|||
"laravel/framework": "5.2.*",
|
||||
"devitek/yaml-translation": "^2.0",
|
||||
"printempw/laravel-datatables-lite": "^1.0",
|
||||
"composer/semver": "^1.4"
|
||||
"composer/semver": "^1.4",
|
||||
"guzzlehttp/guzzle": "^6.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"fzaninotto/faker": "~1.4",
|
||||
"mockery/mockery": "0.9.*",
|
||||
"phpdocumentor/reflection-docblock": "3.2.2",
|
||||
"phpunit/phpunit": "~4.0",
|
||||
"doctrine/instantiator": "1.0.5",
|
||||
"symfony/css-selector": "2.8.*|3.0.*",
|
||||
"symfony/dom-crawler": "2.8.*|3.0.*",
|
||||
"barryvdh/laravel-debugbar": "^2.3",
|
||||
"league/flysystem-memory": "^1.0",
|
||||
"mikey179/vfsStream": "1.6.4"
|
||||
},
|
||||
|
|
@ -45,6 +46,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"post-install-cmd": [
|
||||
"unzip -o storage/patches/bs_column_name_patch_180731.zip",
|
||||
"unzip -o storage/patches/bs_php72_patch_180224.zip"
|
||||
]
|
||||
},
|
||||
|
|
|
|||
1266
composer.lock
generated
1266
composer.lock
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -6,10 +6,23 @@ return [
|
|||
| Application Version
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Version of Blessing Skin Server
|
||||
| Version of Blessing Skin Server.
|
||||
|
|
||||
*/
|
||||
'version' => '3.4.0',
|
||||
'version' => '3.5.0',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Update Source
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Where to get information of new versions.
|
||||
|
|
||||
*/
|
||||
'update_source' => menv(
|
||||
'UPDATE_SOURCE',
|
||||
'https://work.prinzeugen.net/blessing-skin-server/update.json'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
@ -100,7 +113,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'key' => menv('APP_KEY', 'xrjmRQ/RE3B0ICoYqfWzPYizA7MPZz3fNXdnxQ7Cbcg='),
|
||||
'key' => menv('APP_KEY', 'base64:MfnScX0W/ViN8bZtRt0P481rWP3igcOK80QstjbXUxI='),
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
|
|
@ -132,7 +145,7 @@ return [
|
|||
|
||||
'providers' => [
|
||||
|
||||
/*
|
||||
/**
|
||||
* Laravel Framework Service Providers...
|
||||
*/
|
||||
Illuminate\Auth\AuthServiceProvider::class,
|
||||
|
|
@ -157,22 +170,23 @@ return [
|
|||
Illuminate\View\ViewServiceProvider::class,
|
||||
|
||||
/**
|
||||
* Third-party libraries
|
||||
* Third-party Libraries...
|
||||
*/
|
||||
Devitek\Core\Translation\TranslationServiceProvider::class,
|
||||
Swiggles\Memcache\MemcacheServiceProvider::class,
|
||||
Yajra\Datatables\DatatablesServiceProvider::class,
|
||||
|
||||
/**
|
||||
* Application Service Providers...
|
||||
*/
|
||||
App\Providers\BootServiceProvider::class,
|
||||
App\Providers\RuntimeCheckServiceProvider::class,
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\PluginServiceProvider::class,
|
||||
App\Providers\EventServiceProvider::class,
|
||||
App\Providers\LogServiceProvider::class,
|
||||
App\Providers\MemoryServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\PluginServiceProvider::class,
|
||||
App\Providers\ResponseMacroServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\TranslationServiceProvider::class,
|
||||
App\Providers\ValidatorExtendServiceProvider::class,
|
||||
|
||||
],
|
||||
|
|
@ -230,7 +244,6 @@ return [
|
|||
'Option' => App\Services\Facades\Option::class,
|
||||
'Utils' => App\Services\Utils::class,
|
||||
'Minecraft' => App\Services\Minecraft::class,
|
||||
'Updater' => App\Services\Updater::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -109,9 +109,11 @@ return [
|
|||
'cluster' => false,
|
||||
|
||||
'default' => [
|
||||
'scheme' => menv('REDIS_SCHEME', 'tcp'),
|
||||
'host' => menv('REDIS_HOST', 'localhost'),
|
||||
'password' => menv('REDIS_PASSWORD', null),
|
||||
'port' => menv('REDIS_PORT', 6379),
|
||||
'path' => menv('REDIS_SOCKET_PATH'),
|
||||
'password' => menv('REDIS_PASSWORD', null),
|
||||
'database' => 0,
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'driver' => menv('MAIL_DRIVER', 'smtp'),
|
||||
'driver' => menv('MAIL_DRIVER'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
@ -107,6 +107,6 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'sendmail' => '/usr/sbin/sendmail -bs',
|
||||
'sendmail' => menv('SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'),
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ return [
|
|||
'site_name' => 'Blessing Skin',
|
||||
'site_description' => 'Open-source PHP Minecraft Skin Hosting Service',
|
||||
'user_can_register' => 'true',
|
||||
'register_with_player_name' => 'true',
|
||||
'require_verification' => 'false',
|
||||
'regs_per_ip' => '3',
|
||||
'ip_get_method' => '0',
|
||||
'api_type' => 'false',
|
||||
|
|
@ -28,11 +30,11 @@ return [
|
|||
'score_per_player' => '100',
|
||||
'sign_after_zero' => 'false',
|
||||
'version' => '',
|
||||
'check_update' => 'true',
|
||||
'update_source' => 'https://work.prinzeugen.net/update.json',
|
||||
'copyright_text' => '<strong>Copyright © '.getdate()['year'].' <a href="{site_url}">{site_name}</a>.</strong> All rights reserved.',
|
||||
'copyright_text' => '<strong>Copyright © {year} <a href="{site_url}">{site_name}</a>.</strong> All rights reserved.',
|
||||
'auto_del_invalid_texture' => 'false',
|
||||
'return_200_when_notfound' => 'false',
|
||||
'allow_downloading_texture' => 'true',
|
||||
'texture_name_regexp' => '',
|
||||
'return_204_when_notfound' => 'false',
|
||||
'cache_expire_time' => '31536000',
|
||||
'max_upload_file_size' => '1024',
|
||||
'force_ssl' => 'false',
|
||||
|
|
|
|||
|
|
@ -22,4 +22,14 @@ return [
|
|||
|
|
||||
*/
|
||||
'url' => menv('PLUGINS_URL'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Plugins Market Source
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify where to get plugins' metadata for plugin merket.
|
||||
|
|
||||
*/
|
||||
'registry' => menv('PLUGINS_REGISTRY', 'https://work.prinzeugen.net/blessing-skin-server/plugins.json'),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,12 +3,26 @@
|
|||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Configuration about security
|
||||
| Security Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Load them from env to config, preventing cache problems
|
||||
|
|
||||
*/
|
||||
'cipher' => menv('PWD_METHOD', 'SALTED2MD5'),
|
||||
'salt' => menv('SALT', '')
|
||||
'salt' => menv('SALT', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| SSL Certificates
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Describes the SSL certificate verification behavior of all Guzzle requests.
|
||||
| By default, we use the CA bundle provided by Mozilla.
|
||||
|
|
||||
| See: http://docs.guzzlephp.org/en/stable/request-options.html#verify
|
||||
|
|
||||
*/
|
||||
'certificates' => menv('SSL_CERT', storage_path('patches/ca-bundle.crt')),
|
||||
'user_agent' => menv('USER_AGENT', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36'),
|
||||
];
|
||||
|
|
|
|||
48
config/services.php
Normal file
48
config/services.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Stripe, Mailgun, Mandrill, and others. This file provides a sane
|
||||
| default location for this type of information, allowing packages
|
||||
| to have a conventional place to find your various credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'mailgun' => [
|
||||
'domain' => env('MAILGUN_DOMAIN'),
|
||||
'secret' => env('MAILGUN_SECRET'),
|
||||
'guzzle' => [
|
||||
'verify' => config('secure.certificates')
|
||||
],
|
||||
],
|
||||
|
||||
'mandrill' => [
|
||||
'secret' => env('MANDRILL_SECRET'),
|
||||
'guzzle' => [
|
||||
'verify' => config('secure.certificates')
|
||||
],
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('SES_KEY'),
|
||||
'secret' => env('SES_SECRET'),
|
||||
'region' => menv('SES_REGION'),
|
||||
'guzzle' => [
|
||||
'verify' => config('secure.certificates')
|
||||
],
|
||||
],
|
||||
|
||||
'sparkpost' => [
|
||||
'secret' => env('SPARKPOST_SECRET'),
|
||||
'guzzle' => [
|
||||
'verify' => config('secure.certificates')
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -44,7 +44,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'encrypt' => false,
|
||||
'encrypt' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ return [
|
|||
*/
|
||||
|
||||
'paths' => [
|
||||
realpath(base_path('resources/views/overrides')),
|
||||
realpath(base_path('resources/views')),
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,24 @@ $factory->define(User::class, function (Faker\Generator $faker) {
|
|||
'ip' => '127.0.0.1',
|
||||
'permission' => 0,
|
||||
'last_sign_at' => $faker->dateTime,
|
||||
'register_at' => $faker->dateTime
|
||||
'register_at' => $faker->dateTime,
|
||||
'verified' => 1
|
||||
];
|
||||
});
|
||||
|
||||
$factory->defineAs(User::class, 'unverified', function (Faker\Generator $faker) {
|
||||
return [
|
||||
'email' => $faker->email,
|
||||
'nickname' => $faker->name,
|
||||
'score' => 1000,
|
||||
'avatar' => 0,
|
||||
'password' => app('cipher')->hash(str_random(10), config('secure.salt')),
|
||||
'ip' => '127.0.0.1',
|
||||
'permission' => 1,
|
||||
'last_sign_at' => $faker->dateTime,
|
||||
'register_at' => $faker->dateTime,
|
||||
'verified' => 0,
|
||||
'verification_token' => generate_random_token()
|
||||
];
|
||||
});
|
||||
|
||||
|
|
@ -26,7 +43,8 @@ $factory->defineAs(User::class, 'admin', function (Faker\Generator $faker) {
|
|||
'ip' => '127.0.0.1',
|
||||
'permission' => 1,
|
||||
'last_sign_at' => $faker->dateTime,
|
||||
'register_at' => $faker->dateTime
|
||||
'register_at' => $faker->dateTime,
|
||||
'verified' => 1
|
||||
];
|
||||
});
|
||||
|
||||
|
|
@ -40,7 +58,8 @@ $factory->defineAs(User::class, 'superAdmin', function (Faker\Generator $faker)
|
|||
'ip' => '127.0.0.1',
|
||||
'permission' => 2,
|
||||
'last_sign_at' => $faker->dateTime,
|
||||
'register_at' => $faker->dateTime
|
||||
'register_at' => $faker->dateTime,
|
||||
'verified' => 1
|
||||
];
|
||||
});
|
||||
|
||||
|
|
@ -54,6 +73,7 @@ $factory->defineAs(User::class, 'banned', function (Faker\Generator $faker) {
|
|||
'ip' => '127.0.0.1',
|
||||
'permission' => -1,
|
||||
'last_sign_at' => $faker->dateTime,
|
||||
'register_at' => $faker->dateTime
|
||||
'register_at' => $faker->dateTime,
|
||||
'verified' => 1
|
||||
];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,13 +15,20 @@ class ImportOptions extends Migration
|
|||
// import options
|
||||
$options = config('options');
|
||||
|
||||
$options['version'] = config('app.version');
|
||||
$options['version'] = config('app.version');
|
||||
|
||||
$options['announcement'] = str_replace(
|
||||
'{version}',
|
||||
$options['version'],
|
||||
$options['announcement']
|
||||
);
|
||||
|
||||
$options['copyright_text'] = str_replace(
|
||||
'{year}',
|
||||
Carbon\Carbon::now()->year,
|
||||
$options['copyright_text']
|
||||
);
|
||||
|
||||
foreach ($options as $key => $value) {
|
||||
Option::set($key, $value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddVerificationToUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('verified')->default(false);
|
||||
$table->string('verification_token')->default('');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
if (config('database.default') == 'sqlite') {
|
||||
// Dropping columns from a SQLite database requires `doctrine/dbal` dependency.
|
||||
// However, we won't install it because it's too hard to specify the version of
|
||||
// all the new dependencies exactly to make them support PHP ^5.5.9. Damn it.
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('verified');
|
||||
$table->dropColumn('verification_token');
|
||||
});
|
||||
}
|
||||
}
|
||||
34
database/update_scripts/update-3.4.0-to-3.5.0.php
Normal file
34
database/update_scripts/update-3.4.0-to-3.5.0.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
$msg = [];
|
||||
|
||||
try {
|
||||
Artisan::call('migrate', ['--force' => true]);
|
||||
$msg[] = '【数据库】升级成功!现在你可以正常使用 v3.5.0 内置的用户邮箱验证功能了';
|
||||
} catch (Exception $e) {
|
||||
$msg[] = '【数据库】更新数据表失败,错误信息:'.$e->getMessage();
|
||||
$msg[] = '【数据库】这并不影响 v3.5.0 的基本功能,你可以参考 <a href="https://github.com/printempw/blessing-skin-server/wiki/%E6%89%8B%E5%8A%A8%E5%AE%89%E8%A3%85-Blessing-Skin#-%E5%8D%87%E7%BA%A7%E8%87%B3-bs-v350" target="_blank">这篇文章</a> 手动升级你的数据库';
|
||||
}
|
||||
|
||||
$plugins_enabled = (array) json_decode(option('plugins_enabled'), true);
|
||||
|
||||
if (in_array('data-integration', $plugins_enabled)) {
|
||||
$plugins_enabled = '["data-integration"]';
|
||||
$msg[] = '【数据对接】原有的数据对接插件已经不再维护,并且有可能在 v3.5.0 上出现奇怪的问题';
|
||||
$msg[] = '【数据对接】请参考 <a href="https://github.com/printempw/blessing-skin-server/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E6%95%B0%E6%8D%AE%E5%AF%B9%E6%8E%A5" target="_blank">这篇文章</a> 升级你的数据对接插件';
|
||||
} else {
|
||||
$plugins_enabled = '';
|
||||
}
|
||||
|
||||
$msg[] = '【插件系统】升级程序已经自动禁用了所有已安装的插件,因为这些插件的版本可能过旧';
|
||||
$msg[] = '【插件系统】请在后台的「插件市场」页面升级你的所有插件,确保其为最新版后再启用它们';
|
||||
$msg[] = '【插件系统】在 v3.5.0 上强行启用旧版的插件可能造成无法预知的问题!';
|
||||
|
||||
option(['plugins_enabled' => $plugins_enabled]);
|
||||
option(['return_204_when_notfound' => option('return_404_when_notfound')]);
|
||||
option(['version' => config('app.version')]);
|
||||
|
||||
$msg[] = '【升级成功】升级完成后请【务必】清空你的浏览器缓存,否则可能会出现奇怪的问题';
|
||||
$msg[] = '【升级成功】使用愉快!<a href="https://github.com/printempw/blessing-skin-server/wiki/CHANGELOG" target="_blank">查看完整更新日志</a>';
|
||||
|
||||
return $msg;
|
||||
231
gulpfile.js
231
gulpfile.js
|
|
@ -1,27 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
var gulp = require('gulp'),
|
||||
const
|
||||
babel = require('gulp-babel'),
|
||||
eslint = require('gulp-eslint'),
|
||||
uglify = require('gulp-uglify'),
|
||||
stylus = require('gulp-stylus'),
|
||||
chalk = require('chalk'),
|
||||
cleanCss = require('gulp-clean-css'),
|
||||
del = require('del'),
|
||||
exec = require('child_process').exec,
|
||||
concat = require('gulp-concat'),
|
||||
zip = require('gulp-zip'),
|
||||
replace = require('gulp-batch-replace'),
|
||||
notify = require('gulp-notify'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
del = require('del'),
|
||||
eslint = require('gulp-eslint'),
|
||||
execSync = require('child_process').execSync,
|
||||
gulp = require('gulp'),
|
||||
merge = require('merge2'),
|
||||
runSequence = require('run-sequence');
|
||||
replace = require('gulp-batch-replace'),
|
||||
runSequence = require('run-sequence'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
stylus = require('gulp-stylus'),
|
||||
through2 = require('through2'),
|
||||
uglify = require('gulp-uglify'),
|
||||
zip = require('gulp-zip');
|
||||
|
||||
var version = require('./package.json').version;
|
||||
const version = require('./package.json').version;
|
||||
|
||||
var srcPath = 'resources/assets/src';
|
||||
var distPath = 'resources/assets/dist';
|
||||
const srcPath = 'resources/assets/src';
|
||||
const distPath = 'resources/assets/dist';
|
||||
|
||||
var vendorScripts = [
|
||||
const vendorScripts = [
|
||||
'jquery/dist/jquery.min.js',
|
||||
'bootstrap/dist/js/bootstrap.min.js',
|
||||
'admin-lte/dist/js/adminlte.min.js',
|
||||
|
|
@ -33,14 +35,14 @@ var vendorScripts = [
|
|||
'jqPaginator/dist/1.2.0/jqPaginator.min.js',
|
||||
];
|
||||
|
||||
var vendorScriptsToBeMinified = [
|
||||
const vendorScriptsToBeMinified = [
|
||||
'regenerator-runtime/runtime.js',
|
||||
'datatables.net/js/jquery.dataTables.js',
|
||||
'datatables.net-bs/js/dataTables.bootstrap.js',
|
||||
'resources/assets/dist/js/common.js',
|
||||
];
|
||||
|
||||
var vendorStyles = [
|
||||
const vendorStyles = [
|
||||
'bootstrap/dist/css/bootstrap.min.css',
|
||||
'admin-lte/dist/css/AdminLTE.min.css',
|
||||
'datatables.net-bs/css/dataTables.bootstrap.css',
|
||||
|
|
@ -51,22 +53,22 @@ var vendorStyles = [
|
|||
'sweetalert2/dist/sweetalert2.min.css',
|
||||
];
|
||||
|
||||
var styleReplacements = [
|
||||
const styleReplacements = [
|
||||
['blue.png', '"../images/blue.png"'],
|
||||
['blue@2x.png', '"../images/blue@2x.png"'],
|
||||
['../img/loading.gif', '"../images/loading.gif"'],
|
||||
['../img/loading-sm.gif', '"../images/loading-sm.gif"'],
|
||||
];
|
||||
|
||||
var scriptReplacements = [];
|
||||
const scriptReplacements = [];
|
||||
|
||||
var fonts = [
|
||||
const fonts = [
|
||||
'font-awesome/fonts/**',
|
||||
'bootstrap/dist/fonts/**',
|
||||
'resources/assets/src/fonts/**',
|
||||
];
|
||||
|
||||
var images = [
|
||||
const images = [
|
||||
'icheck/skins/square/blue.png',
|
||||
'icheck/skins/square/blue@2x.png',
|
||||
'resources/assets/src/images/**',
|
||||
|
|
@ -74,17 +76,21 @@ var images = [
|
|||
'bootstrap-fileinput/img/loading-sm.gif',
|
||||
];
|
||||
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
|
||||
// Determine if we are in production mode,
|
||||
// run `gulp [task] --production` to enable.
|
||||
if (argv.production) {
|
||||
console.log(chalk.green('>> Running in PRODUCTION mode <<'));
|
||||
process.env.NODE_ENV = 'production';
|
||||
}
|
||||
|
||||
// aka. `yarn run build`
|
||||
gulp.task('default', ['build']);
|
||||
|
||||
// Build the things!
|
||||
gulp.task('build', callback => {
|
||||
runSequence('clean', 'lint', ['compile-es6', 'compile-stylus'], 'publish-vendor', 'notify', callback);
|
||||
});
|
||||
|
||||
// Send a notification
|
||||
gulp.task('notify', () => {
|
||||
return gulp.src('').pipe(notify('Assets compiled!'));
|
||||
runSequence('clean', 'lint', ['compile-scripts', 'compile-stylus'], 'publish-vendor', callback);
|
||||
});
|
||||
|
||||
// Check JavaScript files with ESLint
|
||||
|
|
@ -96,35 +102,39 @@ gulp.task('lint', () => {
|
|||
});
|
||||
|
||||
// Concentrate all vendor scripts & styles to one dist file
|
||||
gulp.task('publish-vendor', ['compile-es6'], callback => {
|
||||
gulp.task('publish-vendor', callback => {
|
||||
// Collect pre-complied and raw library files
|
||||
const vendorJs = gulp.src(collect(vendorScripts)).pipe(replace(scriptReplacements));
|
||||
const rawVendorJs = gulp.src(collect(vendorScriptsToBeMinified)).pipe(uglify());
|
||||
// JavaScript files
|
||||
var js = gulp.src(convertNpmRelativePath(vendorScripts))
|
||||
.pipe(replace(scriptReplacements));
|
||||
var jsToBeMinified = gulp.src(convertNpmRelativePath(vendorScriptsToBeMinified))
|
||||
.pipe(uglify());
|
||||
merge(js, jsToBeMinified)
|
||||
merge(vendorJs, rawVendorJs)
|
||||
.pipe(sourcemaps.init({ loadMaps: true }))
|
||||
.pipe(concat('app.js'))
|
||||
// Remove source mappings in the pre-compiled files
|
||||
.pipe(sourcemaps.write({ addComment: false }))
|
||||
.pipe(gulp.dest(`${distPath}/js/`));
|
||||
// CSS files
|
||||
gulp.src(convertNpmRelativePath(vendorStyles))
|
||||
gulp.src(collect(vendorStyles))
|
||||
.pipe(sourcemaps.init({ loadMaps: true }))
|
||||
.pipe(concat('style.css'))
|
||||
.pipe(replace(styleReplacements))
|
||||
.pipe(sourcemaps.write({ addComment: false }))
|
||||
.pipe(gulp.dest(`${distPath}/css/`));
|
||||
// Fonts
|
||||
gulp.src(convertNpmRelativePath(fonts))
|
||||
gulp.src(collect(fonts))
|
||||
.pipe(gulp.dest(`${distPath}/fonts/`));
|
||||
// Images
|
||||
gulp.src(convertNpmRelativePath(images))
|
||||
gulp.src(collect(images))
|
||||
.pipe(gulp.dest(`${distPath}/images/`));
|
||||
// AdminLTE skins
|
||||
gulp.src(convertNpmRelativePath(['admin-lte/dist/css/skins/*.min.css']))
|
||||
gulp.src(collect(['admin-lte/dist/css/skins/*.min.css']))
|
||||
.pipe(gulp.dest(`${distPath}/css/skins/`));
|
||||
// 3D skin preview
|
||||
gulp.src(convertNpmRelativePath(['three/build/three.min.js', 'skinview3d/build/skinview3d.min.js']))
|
||||
// Libraries for 3D skin preview
|
||||
gulp.src(collect(['three/build/three.min.js', 'skinview3d/build/skinview3d.min.js']))
|
||||
.pipe(concat('skinview3d.js'))
|
||||
.pipe(gulp.dest(`${distPath}/js/`));
|
||||
// Chart.js
|
||||
gulp.src(convertNpmRelativePath(['chart.js/dist/Chart.min.js']))
|
||||
gulp.src(collect(['chart.js/dist/Chart.min.js']))
|
||||
.pipe(concat('chart.js'))
|
||||
.pipe(gulp.dest(`${distPath}/js/`));
|
||||
|
||||
|
|
@ -134,116 +144,123 @@ gulp.task('publish-vendor', ['compile-es6'], callback => {
|
|||
// Compile stylus to css
|
||||
gulp.task('compile-stylus', () => {
|
||||
return gulp.src(`${srcPath}/stylus/*.styl`)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(dev(sourcemaps.init()))
|
||||
.pipe(stylus())
|
||||
.pipe(cleanCss())
|
||||
.pipe(sourcemaps.write('./maps'))
|
||||
.pipe(dev(sourcemaps.write('./maps')))
|
||||
.pipe(gulp.dest(`${distPath}/css`));
|
||||
});
|
||||
|
||||
// Compile ES6 scripts to ES5
|
||||
gulp.task('compile-es6', callback => {
|
||||
gulp.task('compile-scripts', callback => {
|
||||
['common', 'admin', 'auth', 'skinlib', 'user'].forEach(moduleName => {
|
||||
return gulp.src(`${srcPath}/js/${moduleName}/*.js`)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(dev(sourcemaps.init()))
|
||||
.pipe(babel())
|
||||
.pipe(concat(`${moduleName}.js`))
|
||||
.pipe(uglify())
|
||||
.pipe(sourcemaps.write('./maps'))
|
||||
.pipe(dev(sourcemaps.write('./maps')))
|
||||
.pipe(gulp.dest(`${distPath}/js`));
|
||||
});
|
||||
|
||||
callback();
|
||||
});
|
||||
|
||||
// Delete cache files
|
||||
gulp.task('clean', () => {
|
||||
// Delete cache and built files
|
||||
gulp.task('clean', callback => {
|
||||
del([`${distPath}/**/*`]);
|
||||
clearCache();
|
||||
|
||||
return clearDist();
|
||||
callback();
|
||||
});
|
||||
|
||||
// Release archive file
|
||||
// Release a zip archive file
|
||||
// aka. `yarn run release`
|
||||
gulp.task('zip', () => {
|
||||
console.log(`Don't forget to run ${ chalk.underline.yellow('gulp build --production') } first!`);
|
||||
|
||||
console.log('Cleaning cache files');
|
||||
clearCache();
|
||||
console.log('Cache file deleted');
|
||||
|
||||
exec('composer dump-autoload --no-dev', () => {
|
||||
console.log('Autoload files generated without autoload-dev');
|
||||
});
|
||||
// Generate autoload files without autoload-dev
|
||||
execSync('composer dump-autoload --no-dev', { stdio: 'inherit' });
|
||||
|
||||
let zipPath = `blessing-skin-server-v${version}.zip`;
|
||||
const savePath = argv['save-to'] || '..';
|
||||
const zipFile = `blessing-skin-server-v${version}.zip`;
|
||||
|
||||
console.log(`Zip archive will be saved to ${zipPath}.`);
|
||||
console.log('Zip archive will be saved to ' + chalk.underline.blue(
|
||||
require('path').join(savePath, zipFile)
|
||||
));
|
||||
|
||||
return gulp.src([
|
||||
'**/*.*',
|
||||
'artisan',
|
||||
'LICENSE',
|
||||
'!.babelrc',
|
||||
'!.eslintrc.js',
|
||||
'!.eslintignore',
|
||||
'!.editorconfig',
|
||||
'!.travis.yml',
|
||||
'!{.env,.env.testing}',
|
||||
'!{.git,.git/**}',
|
||||
'!{.gitignore,.gitmodules,.gitattributes}',
|
||||
'!gulpfile.js',
|
||||
'!composer.*',
|
||||
'!yarn.lock',
|
||||
'!plugins/**',
|
||||
'plugins/',
|
||||
'!phpunit.xml',
|
||||
'!package.json',
|
||||
'!{tests,tests/**}',
|
||||
'!ISSUE_TEMPLATE.md',
|
||||
'!{coverage,coverage/**}',
|
||||
'!{node_modules,node_modules/**}',
|
||||
'!storage/textures/**',
|
||||
'!resources/assets/{src,src/**}',
|
||||
'!resources/assets/dist/**/{maps,maps/**}',
|
||||
// do not pack packages for developments
|
||||
'!vendor/fzaninotto/**',
|
||||
'!vendor/mockery/**',
|
||||
'!vendor/phpunit/**',
|
||||
'!vendor/symfony/css-selector/**',
|
||||
'!vendor/symfony/dom-crawler/**',
|
||||
'!vendor/mikey179/vfsStream/**',
|
||||
], { dot: true })
|
||||
.pipe(zip(zipPath))
|
||||
.pipe(notify('Don\'t forget to compile Stylus & ES2015 files before publishing a release!'))
|
||||
.pipe(gulp.dest('../'))
|
||||
.pipe(notify({ message: `Zip archive saved to ${zipPath}!` }));
|
||||
'**/*',
|
||||
'**/.gitignore',
|
||||
'**/.htaccess',
|
||||
'.env.example',
|
||||
// Exclude unnecessary files
|
||||
'!.gitignore',
|
||||
'!composer.*',
|
||||
'!gulpfile.js',
|
||||
'!ISSUE_TEMPLATE.md',
|
||||
'!package.json',
|
||||
'!phpunit.xml',
|
||||
'!yarn.lock',
|
||||
// Exclude unnecessary directories
|
||||
'!plugins/**',
|
||||
'!resources/assets/{src,src/**}',
|
||||
'!resources/assets/dist/**/{maps,maps/**}',
|
||||
'!resources/lang/overrides/**',
|
||||
'!resources/views/overrides/**',
|
||||
'!storage/textures/**',
|
||||
'!{coverage,coverage/**}',
|
||||
'!{node_modules,node_modules/**,node_modules/**/.gitignore}',
|
||||
'!{tests,tests/**}',
|
||||
// Extracted symbol links are always weird, I don't know exactly why
|
||||
'!vendor/bin/**',
|
||||
// Exclude "require-dev" packages
|
||||
'!vendor/fzaninotto/**',
|
||||
'!vendor/mikey179/**',
|
||||
'!vendor/mockery/**',
|
||||
'!vendor/phpunit/**',
|
||||
'!vendor/symfony/css-selector/**',
|
||||
'!vendor/symfony/dom-crawler/**',
|
||||
])
|
||||
.pipe(zip(zipFile))
|
||||
.pipe(gulp.dest(savePath))
|
||||
.pipe(through2.obj(function (chunk, enc, callback) {
|
||||
console.log('Zip archive saved!');
|
||||
// Generate autoload files with autoload-dev
|
||||
execSync('composer dump-autoload', { stdio: 'inherit' });
|
||||
callback();
|
||||
}));
|
||||
});
|
||||
|
||||
gulp.task('watch', ['compile-stylus', 'compile-es6'], () => {
|
||||
// watch .scss files
|
||||
gulp.watch(`${srcPath}/stylus/*.scss`, ['compile-stylus'], () => notify('Stylus files compiled!'));
|
||||
// watch .js files
|
||||
gulp.watch(`${srcPath}/js/**/*.js`, ['compile-es6'], () => notify('ES6 scripts compiled!'));
|
||||
gulp.watch(`${srcPath}/js/general.js`, ['publish-vendor']);
|
||||
gulp.task('watch', ['compile-stylus', 'compile-scripts'], () => {
|
||||
gulp.watch(`${srcPath}/stylus/*.styl`, ['compile-stylus']);
|
||||
gulp.watch(`${srcPath}/js/**/*.js`, ['compile-scripts']);
|
||||
gulp.watch(`${srcPath}/js/common/*.js`, ['publish-vendor']);
|
||||
});
|
||||
|
||||
function convertNpmRelativePath(paths) {
|
||||
function dev(transformFunction) {
|
||||
return argv.production ? through2.obj() : transformFunction;
|
||||
}
|
||||
|
||||
const collect = function convertNpmRelativePath(paths) {
|
||||
return paths.map(relativePath => {
|
||||
return relativePath.startsWith('resources') ? relativePath : `node_modules/${relativePath}`;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function clearCache() {
|
||||
return del([
|
||||
'storage/logs/*',
|
||||
'storage/logs/*.log',
|
||||
'storage/testing/*',
|
||||
'storage/debugbar/*',
|
||||
'storage/update_cache/*',
|
||||
'storage/update_cache',
|
||||
'storage/yaml-translation/*',
|
||||
'storage/framework/cache/*',
|
||||
'storage/framework/sessions/*',
|
||||
'storage/framework/views/*'
|
||||
'storage/framework/views/*',
|
||||
'!storage/framework/sessions/index.html'
|
||||
]);
|
||||
}
|
||||
|
||||
function clearDist() {
|
||||
return del([`${distPath}/**/*`]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "blessing-skin-server",
|
||||
"version": "3.4.0",
|
||||
"version": "3.5.0",
|
||||
"description": "A web application brings your custom skins back in offline Minecraft servers.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
"jqPaginator": "^1.2.0",
|
||||
"jquery": "^3.3.1",
|
||||
"regenerator": "^0.12.3",
|
||||
"skinview3d": "^1.1.0-alpha.2",
|
||||
"skinview3d": "^1.1.0",
|
||||
"sweetalert2": "^6.11.5",
|
||||
"toastr": "^2.1.4"
|
||||
},
|
||||
|
|
@ -40,6 +40,7 @@
|
|||
"babel-plugin-transform-inline-environment-variables": "^0.2.0",
|
||||
"babel-plugin-transform-remove-console": "^6.9.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"chalk": "^2.4.1",
|
||||
"codecov": "^3.0.0",
|
||||
"del": "^3.0.0",
|
||||
"gulp": "^3.9.1",
|
||||
|
|
@ -55,6 +56,7 @@
|
|||
"gulp-zip": "^4.1.0",
|
||||
"jest": "^20.0.4",
|
||||
"merge2": "^1.2.1",
|
||||
"minimist": "^1.2.0",
|
||||
"run-sequence": "^2.2.1",
|
||||
"stylus": "^0.54.5"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
const $ = require('jquery');
|
||||
window.$ = window.jQuery = $;
|
||||
$.fn.dataTable = {
|
||||
defaults: {},
|
||||
ext: { errMode: '' },
|
||||
render: { text: () => ({ filter: text => text }) }
|
||||
};
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
|
|
@ -407,16 +412,18 @@ describe('tests for "plugins" module', () => {
|
|||
|
||||
it('enable a plugin', async () => {
|
||||
const fetch = jest.fn()
|
||||
.mockReturnValueOnce(Promise.resolve({ requirements: [] }))
|
||||
.mockReturnValueOnce(Promise.resolve({ requirements: [] }))
|
||||
.mockReturnValueOnce(Promise.resolve({ errno: 0, msg: 'success' }))
|
||||
.mockReturnValueOnce(Promise.resolve({
|
||||
isRequirementsSatisfied: false,
|
||||
requirements: { 'a': '^1.1.0', 'b': '^2.1.0', 'c': '^3.3.0' },
|
||||
unsatisfiedRequirements: { 'c': '^3.3.0' }
|
||||
}))
|
||||
.mockReturnValueOnce(Promise.resolve({ errno: 1, msg: 'notice', reason: ['reason1', 'reason2'] }))
|
||||
.mockReturnValueOnce(Promise.reject());
|
||||
const getPluginDependencies = jest.fn()
|
||||
.mockReturnValueOnce({ dependencies: { requirements: [] } })
|
||||
.mockReturnValue({
|
||||
dependencies: {
|
||||
isRequirementsSatisfied: false,
|
||||
requirements: { 'a': '^1.1.0', 'b': '^2.1.0', 'c': '^3.3.0' },
|
||||
unsatisfiedRequirements: { 'c': '^3.3.0' }
|
||||
}
|
||||
});
|
||||
const url = jest.fn(path => path);
|
||||
const swal = jest.fn()
|
||||
.mockReturnValueOnce(Promise.reject())
|
||||
|
|
@ -435,23 +442,22 @@ describe('tests for "plugins" module', () => {
|
|||
$.pluginsTable = {
|
||||
ajax: {
|
||||
reload: reloadTable
|
||||
}
|
||||
},
|
||||
row: () => ({
|
||||
data: getPluginDependencies
|
||||
})
|
||||
};
|
||||
|
||||
const enablePlugin = require(modulePath).enablePlugin;
|
||||
|
||||
await enablePlugin('plugin');
|
||||
expect(fetch).toBeCalledWith({
|
||||
type: 'POST',
|
||||
url: 'admin/plugins/manage?action=requirements&name=plugin',
|
||||
dataType: 'json'
|
||||
});
|
||||
expect(getPluginDependencies).toBeCalled();
|
||||
expect(swal).toBeCalledWith({
|
||||
text: 'admin.noDependenciesNotice',
|
||||
type: 'warning',
|
||||
showCancelButton: true
|
||||
});
|
||||
expect(fetch.mock.calls.length).toBe(1);
|
||||
expect(fetch).not.toBeCalled();
|
||||
|
||||
await enablePlugin('plugin');
|
||||
expect(fetch).toBeCalledWith({
|
||||
|
|
@ -565,6 +571,160 @@ describe('tests for "plugins" module', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('tests for "market" module', () => {
|
||||
const modulePath = '../admin/market';
|
||||
|
||||
// TODO: test initializing market table
|
||||
|
||||
it('install a plugin', async () => {
|
||||
const fetch = jest.fn()
|
||||
.mockReturnValueOnce(Promise.resolve({ errno: 0, msg: 'success' }))
|
||||
.mockReturnValueOnce(Promise.resolve({ errno: 1, msg: 'failed' }))
|
||||
.mockReturnValueOnce(Promise.reject());
|
||||
const url = jest.fn(path => path);
|
||||
const swal = jest.fn();
|
||||
const toastr = {
|
||||
success: jest.fn(),
|
||||
warning: jest.fn()
|
||||
};
|
||||
const trans = jest.fn(key => key);
|
||||
const reloadTable = jest.fn();
|
||||
const showAjaxError = jest.fn();
|
||||
window.trans = trans;
|
||||
window.fetch = fetch;
|
||||
window.url = url;
|
||||
window.swal = swal;
|
||||
window.toastr = toastr;
|
||||
window.showAjaxError = showAjaxError;
|
||||
$.marketTable = {
|
||||
ajax: {
|
||||
reload: reloadTable
|
||||
}
|
||||
};
|
||||
document.body.innerHTML = `
|
||||
<tr id="plugin-foo-bar"><button class=".btn">Download</button></tr>
|
||||
`;
|
||||
|
||||
const installPlugin = require(modulePath).installPlugin;
|
||||
|
||||
await installPlugin('foo-bar');
|
||||
expect(fetch).toBeCalledWith({
|
||||
type: 'POST',
|
||||
url: 'admin/plugins/market/download',
|
||||
dataType: 'json',
|
||||
data: { name: 'foo-bar' },
|
||||
beforeSend: expect.any(Function)
|
||||
});
|
||||
expect(swal).not.toBeCalledWith({ type: 'warning' });
|
||||
expect(toastr.success).toBeCalledWith('success');
|
||||
expect(reloadTable).toBeCalledWith(null, false);
|
||||
|
||||
await installPlugin('foo-bar');
|
||||
expect(swal).toBeCalledWith({ type: 'warning', html: 'failed' });
|
||||
|
||||
await installPlugin('foo-bar');
|
||||
expect(showAjaxError).toBeCalled();
|
||||
});
|
||||
|
||||
it('update a plugin', async () => {
|
||||
const fetch = jest.fn()
|
||||
.mockReturnValueOnce(Promise.resolve({ errno: 0, msg: 'success' }));
|
||||
const getPluginRowData = jest.fn()
|
||||
.mockReturnValueOnce({ installed: false })
|
||||
.mockReturnValue({
|
||||
title: 'Foo Bar',
|
||||
version: '5.1.4',
|
||||
installed: '1.1.4'
|
||||
});
|
||||
const url = jest.fn(path => path);
|
||||
const swal = jest.fn()
|
||||
.mockReturnValueOnce(Promise.resolve())
|
||||
.mockReturnValueOnce(Promise.reject())
|
||||
.mockReturnValueOnce(Promise.resolve());
|
||||
const toastr = {
|
||||
success: jest.fn(),
|
||||
warning: jest.fn()
|
||||
};
|
||||
const trans = jest.fn(key => key);
|
||||
const reloadTable = jest.fn();
|
||||
const showAjaxError = jest.fn();
|
||||
window.trans = trans;
|
||||
window.fetch = fetch;
|
||||
window.url = url;
|
||||
window.swal = swal;
|
||||
window.toastr = toastr;
|
||||
window.showAjaxError = showAjaxError;
|
||||
$.marketTable = {
|
||||
ajax: {
|
||||
reload: reloadTable
|
||||
},
|
||||
row: () => ({
|
||||
data: getPluginRowData
|
||||
})
|
||||
};
|
||||
document.body.innerHTML = `
|
||||
<tr id="plugin-foo-bar"><button class=".btn">Update</button></tr>
|
||||
`;
|
||||
|
||||
const updatePlugin = require(modulePath).updatePlugin;
|
||||
|
||||
await updatePlugin('foo-bar');
|
||||
expect(swal).toBeCalledWith({ type: 'warning', html: 'not installed' });
|
||||
expect(fetch).not.toBeCalled();
|
||||
|
||||
await updatePlugin('foo-bar');
|
||||
expect(swal).toBeCalledWith({ type: 'warning', text: 'admin.confirmUpdate', showCancelButton: true });
|
||||
expect(fetch).not.toBeCalled();
|
||||
|
||||
await updatePlugin('foo-bar');
|
||||
expect(fetch).toBeCalledWith({
|
||||
type: 'POST',
|
||||
url: 'admin/plugins/market/download',
|
||||
dataType: 'json',
|
||||
data: { name: 'foo-bar' },
|
||||
beforeSend: expect.any(Function)
|
||||
});
|
||||
expect(toastr.success).toBeCalledWith('success');
|
||||
expect(reloadTable).toBeCalledWith(null, false);
|
||||
});
|
||||
|
||||
it('check for plugin updates', async () => {
|
||||
const fetch = jest.fn()
|
||||
.mockReturnValueOnce(Promise.resolve({
|
||||
available: false,
|
||||
plugins: []
|
||||
}))
|
||||
.mockReturnValueOnce(Promise.resolve({
|
||||
available: true,
|
||||
plugins: [{
|
||||
name: 'hello-dolly',
|
||||
version: '8.1.0'
|
||||
}]
|
||||
}));
|
||||
const url = jest.fn(path => path);
|
||||
|
||||
window.fetch = fetch;
|
||||
window.url = url;
|
||||
|
||||
document.body.innerHTML = `
|
||||
<a id="target" href="admin/plugins/market"><i class="fa fa-shopping-bag"></i> <span>Plugin Market</span></a>
|
||||
`;
|
||||
|
||||
const checkForPluginUpdates = require(modulePath).checkForPluginUpdates;
|
||||
|
||||
await checkForPluginUpdates();
|
||||
expect($('#target').html()).toBe(
|
||||
'<i class="fa fa-shopping-bag"></i> <span>Plugin Market</span>'
|
||||
);
|
||||
|
||||
await checkForPluginUpdates();
|
||||
expect($('#target').html()).toBe(
|
||||
'<i class="fa fa-shopping-bag"></i> <span>Plugin Market</span>'+
|
||||
'<span class="label label-success pull-right">1</span>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tests for "update" module', () => {
|
||||
const modulePath = '../admin/update';
|
||||
|
||||
|
|
@ -573,7 +733,8 @@ describe('tests for "update" module', () => {
|
|||
.mockImplementationOnce(({ beforeSend }) => {
|
||||
beforeSend && beforeSend();
|
||||
return Promise.resolve({
|
||||
file_size: 5000
|
||||
release_url: 'http://skin.test/update.zip',
|
||||
tmp_path: '/tmp/update.zip'
|
||||
});
|
||||
})
|
||||
.mockImplementationOnce(() => Promise.resolve())
|
||||
|
|
@ -620,7 +781,6 @@ describe('tests for "update" module', () => {
|
|||
dataType: 'json',
|
||||
}));
|
||||
expect($('#update-button').prop('disabled')).toBe(true);
|
||||
expect($('#file-size').html()).toBe('5000');
|
||||
expect(modal).toBeCalledWith({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
|
|
@ -643,23 +803,30 @@ describe('tests for "update" module', () => {
|
|||
});
|
||||
|
||||
it('download progress polling', async () => {
|
||||
const fetch = jest.fn().mockReturnValueOnce(Promise.resolve({ size: 50 }));
|
||||
const fetch = jest.fn()
|
||||
.mockReturnValueOnce(Promise.resolve([]))
|
||||
.mockReturnValueOnce(Promise.resolve({ total: 810, downloaded: 405 }));
|
||||
const url = jest.fn(path => path);
|
||||
window.fetch = fetch;
|
||||
window.url = url;
|
||||
|
||||
document.body.innerHTML = `
|
||||
<div id="imported-progress"></div>
|
||||
<span id="file-size"></span>
|
||||
<div id="download-progress"></div>
|
||||
<div class="progress-bar"></div>
|
||||
`;
|
||||
|
||||
const { progressPolling } = require(modulePath);
|
||||
await progressPolling(100)();
|
||||
await progressPolling();
|
||||
expect(fetch).toBeCalledWith({
|
||||
url: 'admin/update/download?action=get-file-size',
|
||||
url: 'admin/update/download?action=get-progress',
|
||||
type: 'GET'
|
||||
});
|
||||
expect($('#imported-progress').html()).toBe('50.00');
|
||||
expect($('#file-size').html()).toBe('');
|
||||
|
||||
await progressPolling();
|
||||
expect($('#file-size').html()).toBe('810');
|
||||
expect($('#download-progress').html()).toBe('50.00');
|
||||
expect($('.progress-bar').css('width')).toBe('50%');
|
||||
expect($('.progress-bar').attr('aria-valuenow')).toBe('50.00');
|
||||
});
|
||||
|
|
@ -1156,7 +1323,6 @@ describe('tests for "common" module', () => {
|
|||
const fetch = jest.fn()
|
||||
.mockReturnValue(Promise.resolve({ errno: 0, msg: 'Recorded.' }));
|
||||
|
||||
$.fn.dataTable = { defaults: {}, ext: { errMode: '' } };
|
||||
window.document.cookie = '';
|
||||
window.fetch = fetch;
|
||||
window.blessing = {
|
||||
|
|
@ -1172,6 +1338,7 @@ describe('tests for "common" module', () => {
|
|||
url: 'https://work.prinzeugen.net/statistics/feedback',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
xhr: expect.any(Function),
|
||||
data: { site_name: 'inm', site_url: 'http://tdkr.mur', version: '8.1.0' }
|
||||
});
|
||||
expect(window.document.cookie).not.toBe('');
|
||||
|
|
@ -1185,7 +1352,6 @@ describe('tests for "common" module', () => {
|
|||
const showModal = jest.fn();
|
||||
window.trans = jest.fn(t => t);
|
||||
window.showModal = showModal;
|
||||
$.fn.dataTable = { defaults: {}, ext: { errMode: '' } };
|
||||
|
||||
handleDataTablesAjaxError(undefined, undefined, '{}');
|
||||
expect(showModal).not.toBeCalled();
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ describe('tests for "register" module', () => {
|
|||
document.body.innerHTML = `
|
||||
<input id="email" />
|
||||
<input id="nickname" />
|
||||
<input id="player-name" />
|
||||
<input id="password" />
|
||||
<input id="confirm-pwd" />
|
||||
<div id="captcha-form"></div>
|
||||
|
|
@ -194,6 +195,16 @@ describe('tests for "register" module', () => {
|
|||
expect($('#confirm-pwd').is(':focus')).toBe(true);
|
||||
|
||||
$('#confirm-pwd').val('password');
|
||||
|
||||
// Register with player name
|
||||
$('#nickname').attr('id', 'nickname-alter');
|
||||
$('button').click();
|
||||
expect(trans).toBeCalledWith('auth.emptyPlayerName');
|
||||
expect($('#player-name').is(':focus')).toBe(true);
|
||||
|
||||
// Register with nickname
|
||||
$('#nickname-alter').attr('id', 'nickname');
|
||||
$('#player-name').attr('id', 'player-name-alter');
|
||||
$('button').click();
|
||||
expect(trans).toBeCalledWith('auth.emptyNickname');
|
||||
expect($('#nickname').is(':focus')).toBe(true);
|
||||
|
|
@ -222,7 +233,23 @@ describe('tests for "register" module', () => {
|
|||
expect($('button').prop('disabled')).toBe(true);
|
||||
expect(swal).toBeCalledWith({ type: 'success', html: 'success' });
|
||||
|
||||
// Register with player name
|
||||
$('#nickname').attr('id', 'nickname-alter');
|
||||
$('#player-name-alter').attr('id', 'player-name');
|
||||
$('#player-name').val('Player Name');
|
||||
|
||||
await $('button').click();
|
||||
expect(fetch).toBeCalledWith(expect.objectContaining({
|
||||
type: 'POST',
|
||||
url: 'auth/register',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
email: 'a@b.c',
|
||||
player_name: 'Player Name',
|
||||
password: 'password',
|
||||
captcha: 'captcha'
|
||||
}
|
||||
}));
|
||||
expect(refreshCaptcha).toBeCalled();
|
||||
expect(showMsg).toBeCalledWith('warning', 'warning');
|
||||
expect($('button').html()).toBe('auth.register');
|
||||
|
|
@ -232,6 +259,8 @@ describe('tests for "register" module', () => {
|
|||
});
|
||||
});
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('tests for "forgot" module', () => {
|
||||
const modulePath = '../auth/forgot';
|
||||
|
||||
|
|
@ -263,10 +292,20 @@ describe('tests for "forgot" module', () => {
|
|||
<input id="email" />
|
||||
<div id="captcha-form"></div>
|
||||
<input id="captcha" />
|
||||
<button id="forgot-button"></button>
|
||||
<button id="forgot-button" data-remain="60"></button>
|
||||
`;
|
||||
|
||||
require(modulePath);
|
||||
require(modulePath)();
|
||||
expect(setInterval).toHaveBeenCalledTimes(1);
|
||||
jest.runTimersToTime(1000);
|
||||
|
||||
$('button').click();
|
||||
expect($('button').prop('disabled')).toBe(true);
|
||||
expect(fetch).not.toBeCalled();
|
||||
|
||||
jest.runTimersToTime(60000);
|
||||
expect($('button').html()).toBe('auth.send');
|
||||
expect($('button').prop('disabled')).toBe(false);
|
||||
|
||||
$('button').click();
|
||||
expect(trans).toBeCalledWith('auth.emptyEmail');
|
||||
|
|
@ -294,7 +333,7 @@ describe('tests for "forgot" module', () => {
|
|||
captcha: 'captcha'
|
||||
}
|
||||
}));
|
||||
expect($('button').html()).toBe('auth.send');
|
||||
expect($('button').html()).toEqual(expect.stringContaining('auth.send'));
|
||||
expect($('button').prop('disabled')).toBe(true);
|
||||
expect(showMsg).toBeCalledWith('success', 'success');
|
||||
|
||||
|
|
@ -310,6 +349,8 @@ describe('tests for "forgot" module', () => {
|
|||
});
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
describe('tests for "reset" module', () => {
|
||||
const modulePath = '../auth/reset';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
const $ = require('jquery');
|
||||
window.$ = window.jQuery = $;
|
||||
$.fn.dataTable = { render: { text: () => ({ filter: text => text }) } };
|
||||
|
||||
window.getQueryString = jest.fn((key, defaultValue) => defaultValue);
|
||||
|
||||
|
|
@ -503,12 +504,64 @@ describe('tests for "operations" module', () => {
|
|||
expect(showAjaxError).toBeCalled();
|
||||
});
|
||||
|
||||
it('change texture model', async () => {
|
||||
const fetch = jest.fn()
|
||||
.mockReturnValueOnce(Promise.resolve({ errno: 0, msg: 'success' }))
|
||||
.mockReturnValueOnce(Promise.resolve({ errno: 1, msg: 'warning' }))
|
||||
.mockReturnValueOnce(Promise.reject());
|
||||
window.fetch = fetch;
|
||||
const url = jest.fn(path => path);
|
||||
window.url = url;
|
||||
const trans = jest.fn(key => key);
|
||||
window.trans = trans;
|
||||
const swal = jest.fn()
|
||||
.mockImplementationOnce(() => Promise.reject())
|
||||
.mockImplementationOnce(() => Promise.resolve('alex'));
|
||||
window.swal = swal;
|
||||
const modal = jest.fn();
|
||||
const toastr = {
|
||||
success: jest.fn(),
|
||||
warning: jest.fn()
|
||||
};
|
||||
window.toastr = toastr;
|
||||
const showAjaxError = jest.fn();
|
||||
window.showAjaxError = showAjaxError;
|
||||
|
||||
document.body.innerHTML = '<div id="model"></div>';
|
||||
const changeTextureModel = require(modulePath).changeTextureModel;
|
||||
|
||||
await changeTextureModel(1, 'steve');
|
||||
expect(fetch).not.toBeCalled();
|
||||
|
||||
await changeTextureModel(1, 'steve');
|
||||
expect(swal).toBeCalledWith(expect.objectContaining({
|
||||
text: trans('skinlib.setNewTextureModel'),
|
||||
input: 'select',
|
||||
inputValue: 'steve',
|
||||
}));
|
||||
expect(fetch).toBeCalledWith({
|
||||
type: 'POST',
|
||||
url: 'skinlib/model',
|
||||
dataType: 'json',
|
||||
data: { tid: 1, model: 'alex' }
|
||||
});
|
||||
expect($('div').text()).toBe('alex');
|
||||
expect(toastr.success).toBeCalledWith('success');
|
||||
|
||||
await changeTextureModel(1, 'steve');
|
||||
expect(toastr.warning).toBeCalledWith('warning');
|
||||
|
||||
await changeTextureModel(1, 'steve');
|
||||
expect(showAjaxError).toBeCalled();
|
||||
});
|
||||
|
||||
it('update texture status', () => {
|
||||
window.trans = jest.fn(key => key);
|
||||
document.body.innerHTML = `
|
||||
<div id="likes">5</div>
|
||||
<a tid="1"></a>
|
||||
<a id="1"></a>
|
||||
<a id="quick-apply" style="display: none;"></a>
|
||||
<a class="like" tid="1"></a>
|
||||
<a class="btn" id="1"></a>
|
||||
`;
|
||||
const updateTextureStatus = require(modulePath).updateTextureStatus;
|
||||
|
||||
|
|
@ -519,6 +572,7 @@ describe('tests for "operations" module', () => {
|
|||
expect($('#1').attr('onclick')).toBe('removeFromCloset(1);');
|
||||
expect($('#1').html()).toBe('skinlib.removeFromCloset');
|
||||
expect($('div').html()).toBe('6');
|
||||
expect($('#quick-apply').css('display')).not.toBe('none');
|
||||
|
||||
updateTextureStatus(1, 'remove');
|
||||
expect($('a[tid=1]').attr('onclick')).toBe('addToCloset(1);');
|
||||
|
|
@ -527,6 +581,7 @@ describe('tests for "operations" module', () => {
|
|||
expect($('#1').attr('onclick')).toBe('addToCloset(1);');
|
||||
expect($('#1').html()).toBe('skinlib.addToCloset');
|
||||
expect($('div').html()).toBe('5');
|
||||
expect($('#quick-apply').css('display')).toBe('none');
|
||||
});
|
||||
|
||||
it('click changing privacy button', async () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,55 @@
|
|||
|
||||
const $ = require('jquery');
|
||||
window.$ = window.jQuery = $;
|
||||
$.fn.dataTable = { render: { text: () => ({ filter: text => text }) } };
|
||||
|
||||
describe('tests for "verification" module', () => {
|
||||
const modulePath = '../user/verification';
|
||||
|
||||
it('send verification email', async () => {
|
||||
const url = jest.fn(path => path);
|
||||
const swal = jest.fn();
|
||||
const showAjaxError = jest.fn();
|
||||
window.url = url;
|
||||
window.swal = swal;
|
||||
window.showAjaxError = showAjaxError;
|
||||
|
||||
const fetch = jest.fn()
|
||||
.mockImplementationOnce(option => {
|
||||
option.beforeSend();
|
||||
return Promise.resolve({ errno: 0, msg: 'success' });
|
||||
})
|
||||
.mockImplementationOnce(() => Promise.resolve(
|
||||
{ errno: 1, msg: 'warning' }
|
||||
))
|
||||
.mockImplementationOnce(() => Promise.reject(new Error));
|
||||
window.fetch = fetch;
|
||||
|
||||
document.body.innerHTML = `
|
||||
<a id="send-verification-email">Send</a>
|
||||
<span id="sending-indicator" style="display:none;">Sending</span>
|
||||
`;
|
||||
|
||||
require(modulePath);
|
||||
|
||||
await $('a').click();
|
||||
expect(fetch).toBeCalledWith(expect.objectContaining({
|
||||
type: 'POST',
|
||||
url: 'user/email-verification',
|
||||
dataType: 'json'
|
||||
}));
|
||||
expect(swal).toBeCalledWith({ type: 'success', html: 'success' });
|
||||
// I don't know why $(el).is(':visible') does not work here
|
||||
expect($('#send-verification-email').css('display')).toBe('');
|
||||
expect($('#sending-indicator').css('display')).toBe('none');
|
||||
|
||||
await $('a').click();
|
||||
expect(swal).toBeCalledWith({ type: 'warning', html: 'warning' });
|
||||
|
||||
await $('a').click();
|
||||
expect(showAjaxError).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('tests for "closet" module', () => {
|
||||
const modulePath = '../user/closet';
|
||||
|
|
|
|||
|
|
@ -31,6 +31,17 @@ async function sendFeedback() {
|
|||
site_name: blessing.site_name,
|
||||
site_url: blessing.base_url,
|
||||
version: blessing.version
|
||||
},
|
||||
xhr: () => {
|
||||
// Don't send 'X-CSRF-TOKEN' header to a cross-origin server
|
||||
// @see https://gist.github.com/7kfpun/a8d1326db44aa7857660
|
||||
const xhr = $.ajaxSettings.xhr();
|
||||
const setRequestHeader = xhr.setRequestHeader;
|
||||
xhr.setRequestHeader = function (name, value) {
|
||||
if (name === 'X-CSRF-TOKEN') return;
|
||||
setRequestHeader.call(this, name, value);
|
||||
};
|
||||
return xhr;
|
||||
}
|
||||
});
|
||||
if (errno === 0) {
|
||||
|
|
|
|||
182
resources/assets/src/js/admin/market.js
Normal file
182
resources/assets/src/js/admin/market.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
'use strict';
|
||||
|
||||
if ($('#market-table').length === 1) {
|
||||
$(document).ready(initMarketTable);
|
||||
}
|
||||
|
||||
function initMarketTable() {
|
||||
$.marketTable = $('#market-table').DataTable({
|
||||
columnDefs: marketTableColumnDefs,
|
||||
ajax: {
|
||||
url: url('admin/plugins/market-data'),
|
||||
type: 'POST'
|
||||
}
|
||||
}).on('xhr.dt', handleDataTablesAjaxError);
|
||||
}
|
||||
|
||||
const marketTableColumnDefs = [
|
||||
{
|
||||
targets: 0,
|
||||
title: trans('admin.pluginTitle'),
|
||||
data: 'title',
|
||||
render: (title, type, row) => {
|
||||
const filter = $.fn.dataTable.render.text().filter;
|
||||
return `
|
||||
<strong>${ filter(title) }</strong>
|
||||
<div class="plugin-name">${ filter(row.name) }</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
targets: 1,
|
||||
title: trans('admin.pluginDescription'),
|
||||
data: 'description',
|
||||
render: $.fn.dataTable.render.text(),
|
||||
orderable: false
|
||||
},
|
||||
{
|
||||
targets: 2,
|
||||
title: trans('admin.pluginAuthor'),
|
||||
data: 'author',
|
||||
render: $.fn.dataTable.render.text(),
|
||||
orderable: false
|
||||
},
|
||||
{
|
||||
targets: 3,
|
||||
title: trans('admin.pluginVersion'),
|
||||
data: 'version',
|
||||
render: $.fn.dataTable.render.text(),
|
||||
orderable: false
|
||||
},
|
||||
{
|
||||
targets: 4,
|
||||
title: trans('admin.pluginDependencies'),
|
||||
data: 'dependencies',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
render: data => {
|
||||
if (data.requirements.length === 0) {
|
||||
return `<i>${trans('admin.noDependencies')}</i>`;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
|
||||
for (const name in data.requirements) {
|
||||
const constraint = data.requirements[name];
|
||||
const color = (name in data.unsatisfiedRequirements) ? 'red' : 'green';
|
||||
|
||||
result += `<span class="label bg-${color}">${name}: ${constraint}</span><br>`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
{
|
||||
targets: 5,
|
||||
title: trans('admin.pluginOperations'),
|
||||
orderable: false,
|
||||
render: (data, type, row) => {
|
||||
if (row.installed) {
|
||||
if (row.update_available) {
|
||||
return `
|
||||
<button class="btn btn-success btn-sm" onclick="updatePlugin('${row.name}');">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i> ${ trans('admin.updatePlugin') }
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
if (row.enabled) {
|
||||
return `
|
||||
<button class="btn btn-primary btn-sm" disabled>
|
||||
${ trans('admin.pluginEnabled') }
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<button class="btn btn-primary btn-sm" onclick="enablePlugin('${row.name}');">
|
||||
<i class="fa fa-plug" aria-hidden="true"></i> ${ trans('admin.enablePlugin') }
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<button class="btn btn-default btn-sm" onclick="installPlugin('${row.name}');">
|
||||
<i class="fa fa-download" aria-hidden="true"></i> ${ trans('admin.installPlugin') }
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
async function installPlugin(name, option = {}) {
|
||||
const button = $(`#plugin-${name} .btn`);
|
||||
const originalBtnText = button.html();
|
||||
|
||||
try {
|
||||
const { errno, msg } = await fetch($.extend(true, {
|
||||
type: 'POST',
|
||||
url: url('admin/plugins/market/download'),
|
||||
dataType: 'json',
|
||||
data: { name },
|
||||
beforeSend: () => {
|
||||
button.html(`<i class="fa fa-spinner fa-spin"></i> ${ trans('admin.pluginInstalling') }`).prop('disabled', true);
|
||||
}
|
||||
}, option));
|
||||
|
||||
if (errno === 0) {
|
||||
toastr.success(msg);
|
||||
|
||||
$.marketTable.ajax.reload(null, false);
|
||||
} else {
|
||||
button.html(originalBtnText).prop('disabled', false);
|
||||
swal({ type: 'warning', html: msg });
|
||||
}
|
||||
} catch (error) {
|
||||
button.html(originalBtnText).prop('disabled', false);
|
||||
showAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePlugin(name) {
|
||||
const data = $.marketTable.row(`#plugin-${name}`).data();
|
||||
|
||||
if (data.installed === false) {
|
||||
return swal({ type: 'warning', html: 'not installed' });
|
||||
}
|
||||
|
||||
try {
|
||||
await swal({
|
||||
text: trans('admin.confirmUpdate', { plugin: data.title, old: data.installed, new: data.version }),
|
||||
type: 'warning',
|
||||
showCancelButton: true
|
||||
});
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
installPlugin(name, {
|
||||
beforeSend: () => {
|
||||
$(`#plugin-${name} .btn`).html(`<i class="fa fa-refresh fa-spin"></i> ${ trans('admin.pluginUpdating') }`).prop('disabled', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function checkForPluginUpdates() {
|
||||
try {
|
||||
const data = await fetch({ url: url('admin/plugins/market/check') });
|
||||
if (data.available === true) {
|
||||
const dom = `<span class="label label-success pull-right">${data.plugins.length}</span>`;
|
||||
|
||||
$(`[href="${url('admin/plugins/market')}"]`).append(dom);
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
module.exports = {
|
||||
checkForPluginUpdates,
|
||||
initMarketTable,
|
||||
installPlugin,
|
||||
updatePlugin,
|
||||
};
|
||||
}
|
||||
|
|
@ -5,14 +5,16 @@ if ($('#player-table').length === 1) {
|
|||
}
|
||||
|
||||
function initPlayersTable() {
|
||||
const specificUid = getQueryString('uid');
|
||||
const query = specificUid ? `?uid=${specificUid}` : '';
|
||||
const query = location.href.split('?')[1];
|
||||
|
||||
$('#player-table').DataTable({
|
||||
ajax: url(`admin/player-data${query}`),
|
||||
columnDefs: playersTableColumnDefs,
|
||||
scrollY: ($('.content-wrapper').height() - $('.content-header').outerHeight()) * 0.7,
|
||||
fnDrawCallback: () => $('[data-toggle="tooltip"]').tooltip(),
|
||||
columnDefs: playersTableColumnDefs
|
||||
ajax: {
|
||||
url: url(`admin/player-data${ query ? ('?'+query) : '' }`),
|
||||
type: 'POST'
|
||||
}
|
||||
}).on('xhr.dt', handleDataTablesAjaxError);
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +31,8 @@ const playersTableColumnDefs = [
|
|||
},
|
||||
{
|
||||
targets: 2,
|
||||
data: 'player_name'
|
||||
data: 'player_name',
|
||||
render: $.fn.dataTable.render.text()
|
||||
},
|
||||
{
|
||||
targets: 3,
|
||||
|
|
|
|||
|
|
@ -6,36 +6,61 @@ if ($('#plugin-table').length === 1) {
|
|||
|
||||
function initPluginsTable() {
|
||||
$.pluginsTable = $('#plugin-table').DataTable({
|
||||
ajax: url('admin/plugins/data'),
|
||||
columnDefs: pluginsTableColumnDefs,
|
||||
fnDrawCallback: () => $('[data-toggle="tooltip"]').tooltip(),
|
||||
columnDefs: pluginsTableColumnDefs
|
||||
rowCallback: (row, data) => $(row).addClass(data.enabled ? 'plugin-enabled' : ''),
|
||||
ajax: {
|
||||
url: url('admin/plugins/data'),
|
||||
type: 'POST'
|
||||
}
|
||||
}).on('xhr.dt', handleDataTablesAjaxError);
|
||||
}
|
||||
|
||||
const pluginsTableColumnDefs = [
|
||||
{
|
||||
targets: 0,
|
||||
data: 'title'
|
||||
data: 'title',
|
||||
title: trans('admin.pluginTitle'),
|
||||
width: '10%',
|
||||
render: (title, type, row) => {
|
||||
const actions = [];
|
||||
|
||||
if (row.enabled) {
|
||||
row.config && actions.push(`<a href="${url('admin/plugins/config/'+row.name)}" class="text-primary">${ trans('admin.configurePlugin') }</a>`);
|
||||
actions.push(`<a onclick="disablePlugin('${row.name}');" class="text-primary">${ trans('admin.disablePlugin') }</a>`);
|
||||
} else {
|
||||
actions.push(
|
||||
`<a onclick="enablePlugin('${row.name}');" class="text-primary">${ trans('admin.enablePlugin') }</a>`,
|
||||
`<a onclick="deletePlugin('${row.name}');" class="text-danger">${ trans('admin.deletePlugin') }</a>`
|
||||
);
|
||||
}
|
||||
|
||||
return `
|
||||
<strong>${ $.fn.dataTable.render.text().filter(title) }</strong>
|
||||
<div class="actions">${ actions.join(' | ') }</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
targets: 1,
|
||||
data: 'description',
|
||||
title: trans('admin.pluginDescription'),
|
||||
orderable: false,
|
||||
width: '35%'
|
||||
render: (description, type, row) => {
|
||||
return `
|
||||
<div class="plugin-description"><p>${ $.fn.dataTable.render.text().filter(description) }</p></div>
|
||||
<div class="plugin-version-author">
|
||||
${ trans('admin.pluginVersion') } <span class="text-primary">${ row.version }</span> |
|
||||
${ trans('admin.pluginAuthor') } <a href="${ row.url }">${ row.author }</a> |
|
||||
${ trans('admin.pluginName') } <span>${ row.name }</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
targets: 2,
|
||||
data: 'author',
|
||||
render: data => isEmpty(data.url) ? data.author : `<a href="${data.url}" target="_blank">${data.author}</a>`
|
||||
},
|
||||
{
|
||||
targets: 3,
|
||||
data: 'version',
|
||||
orderable: false
|
||||
},
|
||||
{
|
||||
targets: 4,
|
||||
data: 'dependencies',
|
||||
title: trans('admin.pluginDependencies'),
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
render: data => {
|
||||
|
|
@ -54,45 +79,14 @@ const pluginsTableColumnDefs = [
|
|||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
{
|
||||
targets: 5,
|
||||
data: 'status'
|
||||
},
|
||||
{
|
||||
targets: 6,
|
||||
data: 'operations',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
render: (data, type, row) => {
|
||||
let toggleButton, configViewButton;
|
||||
|
||||
if (data.enabled) {
|
||||
toggleButton = `<a class="btn btn-warning btn-sm" onclick="disablePlugin('${row.name}');">${trans('admin.disablePlugin')}</a>`;
|
||||
} else {
|
||||
toggleButton = `<a class="btn btn-primary btn-sm" onclick="enablePlugin('${row.name}');">${trans('admin.enablePlugin')}</a>`;
|
||||
}
|
||||
|
||||
if (data.enabled && data.hasConfigView) {
|
||||
configViewButton = `<a class="btn btn-default btn-sm" href="${url('/')}admin/plugins/config/${row.name}">${trans('admin.configurePlugin')}</a>`;
|
||||
} else {
|
||||
configViewButton = `<a class="btn btn-default btn-sm" disabled="disabled" title="${trans('admin.noPluginConfigNotice')}" data-toggle="tooltip" data-placement="top">${trans('admin.configurePlugin')}</a>`;
|
||||
}
|
||||
|
||||
const deletePluginButton = `<a class="btn btn-danger btn-sm" onclick="deletePlugin('${row.name}');">${trans('admin.deletePlugin')}</a>`;
|
||||
|
||||
return toggleButton + configViewButton + deletePluginButton;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
async function enablePlugin(name) {
|
||||
const dataTable = $.pluginsTable || $.marketTable;
|
||||
|
||||
try {
|
||||
const { requirements } = await fetch({
|
||||
type: 'POST',
|
||||
url: url(`admin/plugins/manage?action=requirements&name=${name}`),
|
||||
dataType: 'json'
|
||||
});
|
||||
const { requirements } = dataTable.row(`#plugin-${name}`).data().dependencies;
|
||||
|
||||
if (requirements.length === 0) {
|
||||
await swal({
|
||||
|
|
@ -111,7 +105,7 @@ async function enablePlugin(name) {
|
|||
if (errno === 0) {
|
||||
toastr.success(msg);
|
||||
|
||||
$.pluginsTable.ajax.reload(null, false);
|
||||
dataTable.ajax.reload(null, false);
|
||||
} else {
|
||||
swal({ type: 'warning', html: `<p>${msg}</p><ul><li>${reason.join('</li><li>')}</li></ul>` });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
async function downloadUpdates() {
|
||||
console.log('Prepare trno download');
|
||||
console.log('Prepare to download');
|
||||
|
||||
let intervalId;
|
||||
|
||||
try {
|
||||
const preparation = await fetch({
|
||||
|
|
@ -10,16 +12,12 @@ async function downloadUpdates() {
|
|||
dataType: 'json',
|
||||
beforeSend: function() {
|
||||
$('#update-button').html(
|
||||
'<i class="fa fa-spinner fa-spin"></i> ' + trans('admin.preparing')
|
||||
).prop('disabled', 'disabled');
|
||||
`<i class="fa fa-spinner fa-spin"></i> ${ trans('admin.preparing') }`
|
||||
).prop('disabled', true);
|
||||
}
|
||||
});
|
||||
console.log(preparation);
|
||||
|
||||
const { file_size: fileSize } = preparation;
|
||||
|
||||
$('#file-size').html(fileSize);
|
||||
|
||||
$('#modal-start-download').modal({
|
||||
'backdrop': 'static',
|
||||
'keyboard': false
|
||||
|
|
@ -27,8 +25,8 @@ async function downloadUpdates() {
|
|||
|
||||
console.log('Start downloading');
|
||||
|
||||
// Downloading progress polling
|
||||
const interval_id = setInterval(progressPolling(fileSize), 300);
|
||||
// Start downloading progress polling
|
||||
intervalId = setInterval(progressPolling, 1000);
|
||||
|
||||
const download = await fetch({
|
||||
url: url('admin/update/download?action=start-download'),
|
||||
|
|
@ -36,7 +34,7 @@ async function downloadUpdates() {
|
|||
dataType: 'json'
|
||||
});
|
||||
|
||||
clearInterval(interval_id);
|
||||
clearInterval(intervalId);
|
||||
|
||||
console.log('Downloading finished');
|
||||
console.log(download);
|
||||
|
|
@ -51,7 +49,7 @@ async function downloadUpdates() {
|
|||
type: 'POST',
|
||||
dataType: 'json'
|
||||
});
|
||||
|
||||
|
||||
console.log('Package extracted and files are covered');
|
||||
$('#modal-start-download').modal('toggle');
|
||||
|
||||
|
|
@ -65,27 +63,32 @@ async function downloadUpdates() {
|
|||
});
|
||||
} catch (error) {
|
||||
showAjaxError(error);
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}
|
||||
|
||||
function progressPolling(fileSize) {
|
||||
return async () => {
|
||||
try {
|
||||
const { size } = await fetch({
|
||||
url: url('admin/update/download?action=get-file-size'),
|
||||
type: 'GET'
|
||||
});
|
||||
|
||||
const progress = (size / fileSize * 100).toFixed(2);
|
||||
|
||||
$('#imported-progress').html(progress);
|
||||
$('.progress-bar')
|
||||
.css('width', progress + '%')
|
||||
.attr('aria-valuenow', progress);
|
||||
} catch (error) {
|
||||
// No need to show error if failed to get size
|
||||
async function progressPolling() {
|
||||
try {
|
||||
const { total, downloaded } = await fetch({
|
||||
url: url('admin/update/download?action=get-progress'),
|
||||
type: 'GET'
|
||||
});
|
||||
|
||||
if (total === undefined) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const progress = (downloaded / total * 100).toFixed(2);
|
||||
console.log(`Download progress: ${downloaded}/${total}`);
|
||||
|
||||
$('#file-size').html(total);
|
||||
$('#download-progress').html(progress);
|
||||
$('.progress-bar')
|
||||
.css('width', progress + '%')
|
||||
.attr('aria-valuenow', progress);
|
||||
} catch (error) {
|
||||
// No need to show error if failed to get size
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForUpdates() {
|
||||
|
|
|
|||
|
|
@ -5,15 +5,17 @@ if ($('#user-table').length === 1) {
|
|||
}
|
||||
|
||||
function initUsersTable() {
|
||||
const specificUid = getQueryString('uid');
|
||||
const query = specificUid ? `?uid=${specificUid}` : '';
|
||||
const query = location.href.split('?')[1];
|
||||
|
||||
$('#user-table').DataTable({
|
||||
ajax: url(`admin/user-data${query}`),
|
||||
columnDefs: usersTableColumnDefs,
|
||||
scrollY: ($('.content-wrapper').height() - $('.content-header').outerHeight()) * 0.7,
|
||||
fnDrawCallback: () => $('[data-toggle="tooltip"]').tooltip(),
|
||||
rowCallback: (row, data) => $(row).attr('id', `user-${data.uid}`),
|
||||
columnDefs: usersTableColumnDefs
|
||||
ajax: {
|
||||
url: url(`admin/user-data${ query ? ('?'+query) : '' }`),
|
||||
type: 'POST'
|
||||
}
|
||||
}).on('xhr.dt', handleDataTablesAjaxError);
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +38,8 @@ const usersTableColumnDefs = [
|
|||
},
|
||||
{
|
||||
targets: 2,
|
||||
data: 'nickname'
|
||||
data: 'nickname',
|
||||
render: $.fn.dataTable.render.text()
|
||||
},
|
||||
{
|
||||
targets: 3,
|
||||
|
|
@ -58,10 +61,16 @@ const usersTableColumnDefs = [
|
|||
},
|
||||
{
|
||||
targets: 6,
|
||||
data: 'register_at'
|
||||
data: 'verified',
|
||||
className: 'verification',
|
||||
render: data => trans('admin.' + (data ? 'verified' : 'unverified'))
|
||||
},
|
||||
{
|
||||
targets: 7,
|
||||
data: 'register_at'
|
||||
},
|
||||
{
|
||||
targets: 8,
|
||||
data: 'operations',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
|
|
@ -108,6 +117,7 @@ function renderUsersTableOperations(currentUserPermission, type, row) {
|
|||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a onclick="changeUserEmail(${row.uid});">${trans('admin.changeEmail')}</a></li>
|
||||
<li><a onclick="changeUserVerification(${row.uid});">${trans('admin.changeVerification')}</a></li>
|
||||
<li><a onclick="changeUserNickName(${row.uid});">${trans('admin.changeNickName')}</a></li>
|
||||
<li><a onclick="changeUserPwd(${row.uid});">${trans('admin.changePassword')}</a></li>
|
||||
${adminOption}
|
||||
|
|
@ -155,6 +165,31 @@ async function changeUserEmail(uid) {
|
|||
}
|
||||
}
|
||||
|
||||
async function changeUserVerification(uid) {
|
||||
try {
|
||||
const { errno, msg } = await fetch({
|
||||
type: 'POST',
|
||||
url: url('admin/users?action=verification'),
|
||||
dataType: 'json',
|
||||
data: { uid: uid }
|
||||
});
|
||||
|
||||
if (errno === 0) {
|
||||
const original = $(`#user-${uid} > td.verification`).text();
|
||||
|
||||
$(`#user-${uid} > td.verification`).text(
|
||||
original === trans('admin.unverified') ? trans('admin.verified') : trans('admin.unverified')
|
||||
);
|
||||
|
||||
toastr.success(msg);
|
||||
} else {
|
||||
toastr.warning(msg);
|
||||
}
|
||||
} catch (error) {
|
||||
showAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function changeUserNickName(uid) {
|
||||
const dom = $(`tr#user-${uid} > td:nth-child(3)`);
|
||||
let newNickName = '';
|
||||
|
|
@ -342,5 +377,6 @@ if (process.env.NODE_ENV === 'test') {
|
|||
changeAdminStatus,
|
||||
deleteUserAccount,
|
||||
changeUserNickName,
|
||||
changeUserVerification,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ $('#forgot-button').click(e => {
|
|||
}
|
||||
})(data, async () => {
|
||||
try {
|
||||
const { errno, msg } = await fetch({
|
||||
const { errno, msg, remain } = await fetch({
|
||||
type: 'POST',
|
||||
url: url('auth/forgot'),
|
||||
dataType: 'json',
|
||||
|
|
@ -32,20 +32,55 @@ $('#forgot-button').click(e => {
|
|||
beforeSend: () => {
|
||||
$('#forgot-button').html(
|
||||
'<i class="fa fa-spinner fa-spin"></i> ' + trans('auth.sending')
|
||||
).prop('disabled', 'disabled');
|
||||
).prop('disabled', true);
|
||||
}
|
||||
});
|
||||
|
||||
if (errno === 0) {
|
||||
showMsg(msg, 'success');
|
||||
$('#forgot-button').html(trans('auth.send')).prop('disabled', 'disabled');
|
||||
showRemainTimeIndicator(180);
|
||||
} else {
|
||||
showMsg(msg, 'warning');
|
||||
refreshCaptcha();
|
||||
$('#forgot-button').html(trans('auth.send')).prop('disabled', '');
|
||||
|
||||
if (remain) {
|
||||
showRemainTimeIndicator(remain);
|
||||
} else {
|
||||
$('#forgot-button').html(trans('auth.send')).prop('disabled', false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showAjaxError(error);
|
||||
$('#forgot-button').html(trans('auth.send')).prop('disabled', '');
|
||||
$('#forgot-button').html(trans('auth.send')).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function showRemainTimeIndicator(seconds, intervalID) {
|
||||
// Get remain time from elem data if not specified
|
||||
if (seconds === undefined) {
|
||||
seconds = $('#forgot-button').data('remain');
|
||||
}
|
||||
|
||||
if (seconds > 0) {
|
||||
$('#forgot-button').html(`${trans('auth.send')} (${seconds})`).prop('disabled', true);
|
||||
} else {
|
||||
$('#forgot-button').html(trans('auth.send')).prop('disabled', false);
|
||||
// Stop timer
|
||||
if (intervalID) clearInterval(intervalID);
|
||||
}
|
||||
|
||||
// Create timer for decreasing remain time by second
|
||||
if (! intervalID) {
|
||||
const intervalID = window.setInterval(function () {
|
||||
showRemainTimeIndicator(--seconds, intervalID);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Start timer
|
||||
$(document).ready(() => showRemainTimeIndicator());
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
module.exports = showRemainTimeIndicator;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ $('#register-button').click(e => {
|
|||
email: $('#email').val(),
|
||||
password: $('#password').val(),
|
||||
nickname: $('#nickname').val(),
|
||||
player_name: $('#player-name').val(),
|
||||
captcha: $('#captcha').val(),
|
||||
};
|
||||
|
||||
(function validate({ email, password, nickname, captcha }, callback) {
|
||||
(function validate({ email, password, nickname, player_name, captcha }, callback) {
|
||||
// Massive form validation
|
||||
if (email === '') {
|
||||
showMsg(trans('auth.emptyEmail'));
|
||||
|
|
@ -31,9 +32,12 @@ $('#register-button').click(e => {
|
|||
} else if (password !== $('#confirm-pwd').val()) {
|
||||
showMsg(trans('auth.invalidConfirmPwd'), 'warning');
|
||||
$('#confirm-pwd').focus();
|
||||
} else if (nickname === '') {
|
||||
} else if ($('#nickname').length > 0 && nickname === '') {
|
||||
showMsg(trans('auth.emptyNickname'));
|
||||
$('#nickname').focus();
|
||||
} else if ($('#player-name').length > 0 && player_name === '') {
|
||||
showMsg(trans('auth.emptyPlayerName'));
|
||||
$('#player-name').focus();
|
||||
} else if (captcha === '') {
|
||||
showMsg(trans('auth.emptyCaptcha'));
|
||||
$('#captcha').focus();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ function initSkinViewer(cameraPositionZ = 70) {
|
|||
$.msp.viewer = new skinview3d.SkinViewer($.msp.config);
|
||||
$.msp.viewer.camera.position.z = cameraPositionZ;
|
||||
|
||||
// Disable auto model detection
|
||||
$.msp.viewer.detectModel = false;
|
||||
$.msp.viewer.playerObject.skin.slim = $.msp.config.slim;
|
||||
$.msp.viewer.animation = new skinview3d.CompositeAnimation();
|
||||
|
||||
// Init all available animations and pause them
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ console.log(
|
|||
'font-style:italic;', ''
|
||||
);
|
||||
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if given value is empty.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -105,7 +105,10 @@ function renderSkinlibItemComponent(item) {
|
|||
title = item.liked ? trans('skinlib.removeFromCloset') : trans('skinlib.addToCloset');
|
||||
}
|
||||
|
||||
return `<a href="${ url('skinlib/show/' + item.tid) }">
|
||||
item.name = $.fn.dataTable.render.text().filter(item.name);
|
||||
|
||||
return `
|
||||
<a href="${ url('skinlib/show/' + item.tid) }">
|
||||
<div class="item" tid="${ item.tid }">
|
||||
<div class="item-body">
|
||||
<img src="${ url('preview/' + item.tid + '.png') }">
|
||||
|
|
@ -118,6 +121,7 @@ function renderSkinlibItemComponent(item) {
|
|||
</span>
|
||||
</p>
|
||||
|
||||
<span class="pull-right likes-count">${ item.likes }</span>
|
||||
<a title="${title}" class="more like ${liked} ${anonymous}" tid="${ item.tid }" href="javascript:;" data-placement="top" data-toggle="tooltip"><i class="fa fa-heart"></i></a>
|
||||
|
||||
<small class="more private-label ${(item.public === 0) ? '' : 'hide'}" tid="${ item.tid }">
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ function addToCloset(tid) {
|
|||
try {
|
||||
const result = await swal({
|
||||
title: trans('skinlib.setItemName'),
|
||||
text: trans('skinlib.applyNotice'),
|
||||
inputValue: name,
|
||||
input: 'text',
|
||||
showCancelButton: true,
|
||||
|
|
@ -130,6 +131,47 @@ async function changeTextureName(tid, oldName) {
|
|||
}
|
||||
}
|
||||
|
||||
async function changeTextureModel(tid, oldModel) {
|
||||
const models = {
|
||||
steve: 'steve',
|
||||
alex: 'alex',
|
||||
cape: trans('general.cape')
|
||||
};
|
||||
|
||||
let newTextureModel = '';
|
||||
|
||||
try {
|
||||
newTextureModel = await swal({
|
||||
text: trans('skinlib.setNewTextureModel'),
|
||||
input: 'select',
|
||||
inputValue: oldModel,
|
||||
inputOptions: models,
|
||||
showCancelButton: true,
|
||||
inputClass: 'form-control'
|
||||
});
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { errno, msg } = await fetch({
|
||||
type: 'POST',
|
||||
url: url('skinlib/model'),
|
||||
dataType: 'json',
|
||||
data: { tid: tid, model: newTextureModel }
|
||||
});
|
||||
|
||||
if (errno === 0) {
|
||||
$('#model').text(models[newTextureModel]);
|
||||
toastr.success(msg);
|
||||
} else {
|
||||
toastr.warning(msg);
|
||||
}
|
||||
} catch (error) {
|
||||
showAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update button action & likes of texture.
|
||||
*
|
||||
|
|
@ -138,17 +180,22 @@ async function changeTextureName(tid, oldName) {
|
|||
* @return {null}
|
||||
*/
|
||||
function updateTextureStatus(tid, action) {
|
||||
const likes = parseInt($('#likes').html()) + (action === 'add' ? 1 : -1);
|
||||
action = (action === 'add') ? 'removeFromCloset' : 'addToCloset';
|
||||
const likesCounter = $('#likes').length ? $('#likes') : $(`.item[tid=${tid}] .likes-count`);
|
||||
const likes = parseInt(likesCounter.html()) + (action === 'add' ? 1 : -1);
|
||||
const buttonAction = (action === 'add') ? 'removeFromCloset' : 'addToCloset';
|
||||
likesCounter.html(likes);
|
||||
|
||||
$(`a[tid=${tid}]`)
|
||||
.attr('onclick', `${action}(${tid});`)
|
||||
.attr('title', trans(`skinlib.${action}`))
|
||||
// On "skinlib" page
|
||||
$(`a.like[tid=${tid}]`)
|
||||
.attr('onclick', `${buttonAction}(${tid});`)
|
||||
.attr('title', trans(`skinlib.${buttonAction}`))
|
||||
.toggleClass('liked');
|
||||
$(`#${tid}`)
|
||||
.attr('onclick', `${action}(${tid});`)
|
||||
.html(trans(`skinlib.${action}`));
|
||||
$('#likes').html(likes);
|
||||
|
||||
// On "skinlib/show" page
|
||||
$(`.btn#${tid}`)
|
||||
.attr('onclick', `${buttonAction}(${tid});`)
|
||||
.html(trans(`skinlib.${buttonAction}`));
|
||||
$('#quick-apply').toggle(action === 'add');
|
||||
}
|
||||
|
||||
$(document).on('click', '.private-label', async function () {
|
||||
|
|
@ -231,6 +278,7 @@ if (process.env.NODE_ENV === 'test') {
|
|||
ajaxAddToCloset,
|
||||
removeFromCloset,
|
||||
changeTextureName,
|
||||
changeTextureModel,
|
||||
updateTextureStatus,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* global initSkinViewer, defaultSteveSkin, defaultAlexSkin */
|
||||
/* global skinview3d, initSkinViewer, defaultSteveSkin, defaultAlexSkin */
|
||||
|
||||
// TODO: Help wanted. This file needs to be tested.
|
||||
|
||||
|
|
@ -11,14 +11,14 @@ function initUploadListeners() {
|
|||
|
||||
$('body')
|
||||
.on('change', '#file', () => handleFiles())
|
||||
.on('filebatchselected', '#file', () => handleFiles())
|
||||
.on('ifToggled', '#type-cape', () => handleFiles())
|
||||
.on('change', '#skin-type', function () {
|
||||
if ($('#file').prop('files').length === 0) {
|
||||
$.msp.config.slim = ($(this).val() === 'alex');
|
||||
$.msp.config.skinUrl = getDefaultSkin();
|
||||
initSkinViewer();
|
||||
// Load default skin
|
||||
$.msp.viewer.skinUrl = getDefaultSkin();
|
||||
}
|
||||
handleFiles();
|
||||
$.msp.viewer.playerObject.skin.slim = ($(this).val() === 'alex');
|
||||
})
|
||||
.on('ifToggled', '#type-skin', function () {
|
||||
$(this).prop('checked') ? $('#skin-type').show() : $('#skin-type').hide();
|
||||
|
|
@ -66,14 +66,16 @@ function handleFiles(files, type) {
|
|||
if (img.width === img.height || img.width / img.height === 2) {
|
||||
$.msp.config.skinUrl = img.src;
|
||||
$.msp.config.capeUrl = null;
|
||||
|
||||
// Determine model from texture image
|
||||
$.msp.config.slim = skinview3d.isSlimSkin(img);
|
||||
$('#skin-type').val($.msp.config.slim ? 'alex' : 'steve');
|
||||
} else {
|
||||
$.msp.config.skinUrl = getDefaultSkin();
|
||||
toastr.warning(trans('skinlib.badSkinSize'));
|
||||
}
|
||||
}
|
||||
|
||||
$.msp.config.slim = ($('#skin-type').val() === 'alex');
|
||||
|
||||
initSkinViewer();
|
||||
|
||||
if ($name.val() === '' || $name.val() === $name.attr('data-last-file-name')) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,16 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
$(document).ready(initCloset);
|
||||
$(document).ready(async () => {
|
||||
await initCloset();
|
||||
|
||||
const tid = getQueryString('tid');
|
||||
|
||||
// Select closet item if texture id is specified by query string
|
||||
if (tid !== undefined) {
|
||||
$(`[tid=${tid}]`).children('.item-body').click();
|
||||
}
|
||||
});
|
||||
|
||||
$('body').on('click', '.item-body', async function () {
|
||||
$('.item-selected').parent().removeClass('item-selected');
|
||||
|
|
@ -106,26 +115,30 @@ async function initCloset() {
|
|||
* @param {{ name: string, tid: number, type: 'steve' | 'alex' | 'cape' }} item
|
||||
*/
|
||||
function renderClosetItemComponent(item) {
|
||||
// Prevent XSS
|
||||
item.name = $.fn.dataTable.render.text().filter(item.name);
|
||||
|
||||
return `
|
||||
<div class="item" tid="${item.tid}" data-texture-type="${item.type}">
|
||||
<div class="item-body">
|
||||
<img src="${url('/')}preview/${item.tid}.png">
|
||||
</div>
|
||||
<div class="item-footer">
|
||||
<p class="texture-name">
|
||||
<span title="${item.name}">${item.name} <small>(${item.type})</small></span>
|
||||
</p>
|
||||
<div class="item-body">
|
||||
<img src="${url('/')}preview/${item.tid}.png">
|
||||
</div>
|
||||
<div class="item-footer">
|
||||
<p class="texture-name">
|
||||
<span title="${item.name}">${item.name} <small>(${item.type})</small></span>
|
||||
</p>
|
||||
|
||||
<a href="${url('/')}skinlib/show/${item.tid}" title="${trans('user.viewInSkinlib')}" class="more" data-toggle="tooltip" data-placement="bottom"><i class="fa fa-share"></i></a>
|
||||
<span title="${trans('general.more')}" class="more" data-toggle="dropdown" aria-haspopup="true" id="more-button"><i class="fa fa-cog"></i></span>
|
||||
<a href="${url('/')}skinlib/show/${item.tid}" title="${trans('user.viewInSkinlib')}" class="more" data-toggle="tooltip" data-placement="bottom"><i class="fa fa-share"></i></a>
|
||||
<span title="${trans('general.more')}" class="more" data-toggle="dropdown" aria-haspopup="true" id="more-button"><i class="fa fa-cog"></i></span>
|
||||
|
||||
<ul class="dropup dropdown-menu" aria-labelledby="more-button">
|
||||
<li><a onclick="renameClosetItem(${item.tid}, '${item.name}');">${trans('user.renameItem')}</a></li>
|
||||
<li><a onclick="removeFromCloset(${item.tid});">${trans('user.removeItem')}</a></li>
|
||||
<li><a onclick="setAsAvatar(${item.tid});">${trans('user.setAsAvatar')}</a></li>
|
||||
</ul>
|
||||
<ul class="dropup dropdown-menu" aria-labelledby="more-button">
|
||||
<li><a onclick="renameClosetItem(${item.tid}, '${item.name}');">${trans('user.renameItem')}</a></li>
|
||||
<li><a onclick="removeFromCloset(${item.tid});">${trans('user.removeItem')}</a></li>
|
||||
<li><a onclick="setAsAvatar(${item.tid});">${trans('user.setAsAvatar')}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
24
resources/assets/src/js/user/verification.js
Normal file
24
resources/assets/src/js/user/verification.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
$('#send-verification-email').click(async () => {
|
||||
try {
|
||||
const { errno, msg } = await fetch({
|
||||
type: 'POST',
|
||||
url: url('user/email-verification'),
|
||||
dataType: 'json',
|
||||
beforeSend: () => {
|
||||
$('#send-verification-email').hide();
|
||||
$('#sending-indicator').show();
|
||||
}
|
||||
});
|
||||
|
||||
swal({
|
||||
type: errno === 0 ? 'success' : 'warning',
|
||||
html: msg
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
showAjaxError(error);
|
||||
}
|
||||
|
||||
$('#send-verification-email').show();
|
||||
$('#sending-indicator').hide();
|
||||
});
|
||||
|
|
@ -45,3 +45,51 @@ td {
|
|||
color: #3c8dbc;
|
||||
}
|
||||
}
|
||||
|
||||
#plugin-table {
|
||||
.actions {
|
||||
margin-top: 5px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.plugin-version-author {
|
||||
color: #777;
|
||||
font-size: small;
|
||||
|
||||
a {
|
||||
color: #337ab7;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
border-left: 3px solid transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.plugin-enabled {
|
||||
background-color: #f7fcfe;
|
||||
}
|
||||
|
||||
.plugin-enabled td:first-child {
|
||||
border-left-color: #3c8dbc;
|
||||
}
|
||||
}
|
||||
|
||||
#market-table {
|
||||
.btn {
|
||||
float: right;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.plugin-name {
|
||||
color: #777;
|
||||
font-size: small;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,3 +215,12 @@ input:-webkit-autofill {
|
|||
td[class='key'], td[class='value'] {
|
||||
border-top: 0 !important;
|
||||
}
|
||||
|
||||
.modal-danger .modal-title small {
|
||||
color: #fff;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ users:
|
|||
banned: Banned
|
||||
admin: Admin
|
||||
super-admin: Super Admin
|
||||
verification:
|
||||
title: Account Verification
|
||||
operations:
|
||||
title: Operations
|
||||
non-existent: No such user.
|
||||
|
|
@ -25,6 +27,8 @@ users:
|
|||
change: Edit Email
|
||||
existed: :email is existed.
|
||||
success: Email changed successfully.
|
||||
verification:
|
||||
success: Account verification status switched successfully.
|
||||
nickname:
|
||||
change: Edit Nickname
|
||||
success: Nickname changed successfully.
|
||||
|
|
@ -46,21 +50,21 @@ users:
|
|||
success: The account has been banned.
|
||||
unban:
|
||||
text: Unban
|
||||
success: The account has been unbanned.
|
||||
cant-super-admin: You can't ban super admin.
|
||||
success: The account was not banned anymore.
|
||||
cant-super-admin: You can't ban a super admin.
|
||||
cant-admin: Only super admins are able to ban admins.
|
||||
delete:
|
||||
delete: Delete User
|
||||
success: The account has been deleted successfully.
|
||||
cant-super-admin: You can't delete super admin in this way
|
||||
cant-admin: You can't delete admins.
|
||||
cant-super-admin: You can't delete a super admin.
|
||||
cant-admin: You can't delete a admin account.
|
||||
|
||||
players:
|
||||
no-permission: You have no permission to operate this player.
|
||||
operations:
|
||||
title: Operations
|
||||
preference:
|
||||
success: The preference of player [:player] has been changed to :preference
|
||||
success: The preference of player ":player" has been changed to :preference
|
||||
textures:
|
||||
change: Change Textures
|
||||
non-existent: No such texture tid.:tid
|
||||
|
|
@ -76,7 +80,7 @@ players:
|
|||
|
||||
customize:
|
||||
change-color:
|
||||
title: Change theme color
|
||||
title: Change Theme Color
|
||||
success: Theme color updated.
|
||||
|
||||
colors:
|
||||
|
|
@ -94,17 +98,6 @@ customize:
|
|||
black-light: Black Light
|
||||
|
||||
plugins:
|
||||
name: Name
|
||||
description: Description
|
||||
author: Author
|
||||
version: Version
|
||||
dependencies: Dependencies
|
||||
|
||||
status:
|
||||
title: Status
|
||||
enabled: Enabled
|
||||
disabled: Disabled
|
||||
|
||||
operations:
|
||||
title: Operations
|
||||
enabled: :plugin has been enabled.
|
||||
|
|
@ -114,9 +107,17 @@ plugins:
|
|||
version: The version of <code>:name</code> does not satisfies the constraint <code>:constraint</code>
|
||||
disabled: :plugin has been disabled.
|
||||
deleted: The plugin was deleted successfully.
|
||||
no-config-notice: The plugin is not installed or doesn't provide configuration page.
|
||||
no-config-notice: The plugin is not installed or doesn't provide a configuration page.
|
||||
not-found: No such plugin.
|
||||
|
||||
market:
|
||||
connection-error: Unable to connect to the plugins registry (to change the plugins registry URL, please refer to http://t.cn/Rk6X37l). :error
|
||||
non-existent: The plugin :plugin does not exist.
|
||||
download-failed: Unable to download the plugin. :error
|
||||
shasum-failed: The downloaded file failed hash check, please retry.
|
||||
unzip-failed: Unable to extract the plugin. :error
|
||||
install-success: Plugin was installed.
|
||||
|
||||
empty: No result
|
||||
|
||||
update:
|
||||
|
|
@ -142,7 +143,7 @@ update:
|
|||
|
||||
downloads:
|
||||
text: "Download Link:"
|
||||
link: Click to download full program package.
|
||||
link: Click to download full update package.
|
||||
|
||||
check-github: <a href=":url" target="_blank" class="btn btn-default pull-right">Check GitHub Releases</a>
|
||||
button: Update Now
|
||||
|
|
@ -150,18 +151,19 @@ update:
|
|||
cautions:
|
||||
title: Cautions
|
||||
text: |
|
||||
Please choose update source according to your host location.
|
||||
Low-speed connection between update source and your host will cause long-time loading at checking/downloading page.
|
||||
Please choose update source according to your host's network environment.
|
||||
Low-speed connection between update source and your host will cause long-time loading at checking and downloading page.
|
||||
To change the default update source, please refer to <a target="_blank" href="https://github.com/printempw/blessing-skin-server/wiki/%E6%9B%B4%E6%96%B0%E6%BA%90%E5%88%97%E8%A1%A8">this wiki article</a>.
|
||||
|
||||
download:
|
||||
downloading: Downloading update package...
|
||||
size: "Size of package:"
|
||||
|
||||
errors:
|
||||
errors:
|
||||
prefix: "An error occured: "
|
||||
connection: "Unable to access to current update source. Details:"
|
||||
write-permission: Unable to make cache directory. Please sure permission.
|
||||
unzip: "Failed to unzip update file. Error code: "
|
||||
connection: We can't connect to the update source. :error
|
||||
write-permission: Unable to create the cache directory. Please check the permission.
|
||||
unzip: "Failed to extract update package. Error code: "
|
||||
overwrite: Unable to overwrite files.
|
||||
|
||||
invalid-action: Invalid action
|
||||
|
|
|
|||
|
|
@ -3,70 +3,80 @@ login:
|
|||
button: Log In
|
||||
message: Log in to manage your skin & players
|
||||
keep: Remember me
|
||||
success: Logged in successfully~
|
||||
success: Logged in successfully.
|
||||
|
||||
check:
|
||||
anonymous: Illegal access. Please log in first.
|
||||
verified: To access this page, you should verify your email address first.
|
||||
admin: Only admins are permitted to access this page.
|
||||
super-admin: Only super admin is permitted to access this page.
|
||||
banned: You are banned on this site. Please contact the admin.
|
||||
token: Invalid token. Please log in.
|
||||
token: Token expired. Please log in.
|
||||
|
||||
register:
|
||||
title: Register
|
||||
button: Register
|
||||
message: Welcome to :sitename!
|
||||
player-name-intro: Player name in Minecraft, can be changed later
|
||||
nickname-intro: Whatever you like expect special characters
|
||||
repeat-pwd: Repeat your password
|
||||
close: Well, this site doesn't allow any register.
|
||||
success: Registered successfully. Redirecting...
|
||||
close: We don't accept any registration.
|
||||
success: Your account was registered. Redirecting...
|
||||
max: You can't register more than :regs accounts.
|
||||
registered: The email address is already registered.
|
||||
registered: The email address was already registered.
|
||||
|
||||
forgot:
|
||||
title: Forgot Password
|
||||
button: Send
|
||||
message: We will send you an E-mail to verify.
|
||||
login-link: I do remember it
|
||||
close: Password resetting is not available now.
|
||||
frequent-mail: You click the send button too fast. Wait for 60 secs, guy.
|
||||
disabled: Password resetting is not available.
|
||||
frequent-mail: You click the send button too fast. Wait for some minutes.
|
||||
unregistered: The email address is not registered.
|
||||
|
||||
mail:
|
||||
title: Reset your password on :sitename
|
||||
success: Mail is sent. Will be expired in 1 hour, please check.
|
||||
failed: Fail to send mail, detailed message :msg
|
||||
message: You are receiving this email because this email address was used to reset your password on :sitename
|
||||
ignore: If you haven't signed up on our site, please ignore this email. No unsubscribing is required.
|
||||
reset: Reset your password
|
||||
notice: This mail is sending automatically, no reponses will be sent if you reply.
|
||||
success: Mail sent, please check your inbox. The link will be expired in 1 hour.
|
||||
failed: Failed to send verification mail. :msg
|
||||
mail:
|
||||
title: Reset your password on :sitename
|
||||
message: You are receiving this email because we received a password reset request for your account on :sitename.
|
||||
reset: 'To reset your password, please visit: <a href=":url">:url</a>'
|
||||
ignore: If you did not request a password reset, no further action is required.
|
||||
|
||||
reset:
|
||||
title: Reset Password
|
||||
button: Reset
|
||||
invalid: Invalid link.
|
||||
expired: This link is expired.
|
||||
message: :username, reset your email address here.
|
||||
success: Password resetted successfully.
|
||||
expired: This link is expired, please resend a verification email.
|
||||
message: :username, reset your password here.
|
||||
success: Your password was reset successfully.
|
||||
|
||||
bind:
|
||||
title: Bind Email
|
||||
button: Bind
|
||||
message: You need to fill your email adderss to continue.
|
||||
introduction: Email addresses will be used for password resetting. We won't send you any spam.
|
||||
registered: The email address is already registered.
|
||||
message: You need to provide your email adderss to continue.
|
||||
introduction: We won't send you any spam.
|
||||
registered: The email address was already taken.
|
||||
|
||||
verify:
|
||||
title: Email Verification
|
||||
success: Your account was now verified.
|
||||
message: Welcome to :sitename!
|
||||
button: Homepage
|
||||
invalid: Invalid link.
|
||||
expired: This link is expired, please resend a verification email.
|
||||
|
||||
validation:
|
||||
identification: Invalid format of email or player name.
|
||||
identification: The email or player name is invalid.
|
||||
email: Email format is invalid.
|
||||
captcha: Wrong CAPTCHA.
|
||||
user: Unexistent user.
|
||||
user: No such user.
|
||||
password: Wrong password.
|
||||
|
||||
logout:
|
||||
success: Logged out successfully~
|
||||
success: You are now logged out.
|
||||
fail: No valid session.
|
||||
|
||||
nickname: Nickname
|
||||
player-name: Minecraft player name
|
||||
email: Email
|
||||
identification: Email or player name
|
||||
password: Password
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
http:
|
||||
msg-403: You have no permission to access this page.
|
||||
msg-404: Nothing here.
|
||||
msg-500: Please try again later.
|
||||
msg-503: The application is now in maintenance mode.
|
||||
method-not-allowed: Method not allowed.
|
||||
csrf-token-mismatch: Token does not match, try reloading the page.
|
||||
|
||||
general:
|
||||
title: Error occurred
|
||||
|
|
@ -15,3 +18,6 @@ exception:
|
|||
plugins:
|
||||
duplicate: The plugin [:dir1] has a duplicated plugin name definition which is same to plugin [:dir2]. Please check your plugins directory, remove one of them or use another name definition.
|
||||
directory: We can't approach the path for loading plugins specified by the PLUGINS_DIR in .env file. Please check your configuration. Error :msg
|
||||
|
||||
cipher:
|
||||
unsupported: Unsupported password hashing method `:cipher`, please check your `.env` configuration
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ plugin-market: Plugin Market
|
|||
plugin-configs: Plugin Configs
|
||||
customize: Customize
|
||||
options: Options
|
||||
import-v2: Import Data
|
||||
score-options: Score Options
|
||||
check-update: Check Update
|
||||
download-update: Download Updates
|
||||
|
|
@ -39,19 +38,19 @@ reset: Reset
|
|||
submit: Submit
|
||||
|
||||
notice: Notice
|
||||
switch-2d-preview: Switch to 2D Preview
|
||||
switch-2d-preview: Switch to 2D preview
|
||||
|
||||
illegal-parameters: Illegal parameters.
|
||||
|
||||
private: Private
|
||||
public: Public
|
||||
|
||||
unexistent-user: Un-existent user
|
||||
unexistent-player: Un-existent player
|
||||
player-banned: The owner of this player has been banned
|
||||
unexistent-user: No such user.
|
||||
unexistent-player: No such player.
|
||||
player-banned: The owner of this player has been banned.
|
||||
|
||||
texture-deleted: The requested texture has been deleted.
|
||||
texture-not-uploaded: The user haven not uploaded the texture of :type model yet.
|
||||
texture-not-uploaded: The user haven't uploaded the texture of :type model yet.
|
||||
|
||||
operations: Operations
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
features:
|
||||
multi-player:
|
||||
first:
|
||||
icon: fa-users
|
||||
name: Multi Player
|
||||
desc: You can add many players with only one account.
|
||||
desc: You can add multiple players with only one account.
|
||||
|
||||
sharing:
|
||||
second:
|
||||
icon: fa-share-alt
|
||||
name: Sharing
|
||||
desc: Explore the skin library, give a 'like' and share them with your friends.
|
||||
desc: Explore the skin library, send a "like" and share them with your friends.
|
||||
|
||||
free:
|
||||
third:
|
||||
icon: fa-cloud
|
||||
name: Free
|
||||
desc: It is free forever. No ads. No subscription fees.
|
||||
|
||||
introduction: :sitename provides the service of uploading and hosting Minecraft skins. By coordinating with skin mods e.g. CustomSkinLoader, you can set skins & capes for your game character, and make it visible to other players.
|
||||
introduction: :sitename provides the service of uploading and hosting Minecraft skins. By coordinating with skin mods (e.g. CustomSkinLoader), you can choose skin and cape for your game character, and make it visible to other players in Minecraft.
|
||||
|
||||
start: Join Us
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
/*!
|
||||
* Blessing Skin English Translations
|
||||
*
|
||||
* @see https://github.com/printempw/blessing-skin-server
|
||||
* @author printempw <h@prinzeugen.net>
|
||||
*
|
||||
* NOTE: this file must be saved in UTF-8 encoding.
|
||||
*/
|
||||
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
|
|
@ -15,7 +6,7 @@
|
|||
// Login
|
||||
emptyIdentification: 'Empty email/player name.',
|
||||
emptyPassword: 'Password is required.',
|
||||
emptyCaptcha: 'Empty password.',
|
||||
emptyCaptcha: 'Please enter the CAPTCHA.',
|
||||
login: 'Log In',
|
||||
loggingIn: 'Logging In',
|
||||
tooManyFails: 'You fails too many times! Please enter the CAPTCHA.',
|
||||
|
|
@ -27,6 +18,7 @@
|
|||
emptyConfirmPwd: 'Empty confirming password.',
|
||||
invalidConfirmPwd: 'Confirming password is not equal with password.',
|
||||
emptyNickname: 'Empty nickname.',
|
||||
emptyPlayerName: 'Empty player name.',
|
||||
register: 'Register',
|
||||
registering: 'Registering',
|
||||
|
||||
|
|
@ -41,12 +33,16 @@
|
|||
addToCloset: 'Add to closet',
|
||||
removeFromCloset: 'Remove from closet',
|
||||
setItemName: 'Set a name for this texture',
|
||||
applyNotice: 'You can apply it to player at your closet',
|
||||
emptyItemName: 'Empty texture name.',
|
||||
|
||||
// Rename
|
||||
setNewTextureName: 'Please enter the new texture name:',
|
||||
emptyNewTextureName: 'Empty new texture name.',
|
||||
|
||||
// Change Model
|
||||
setNewTextureModel: 'Please select a new texture model:',
|
||||
|
||||
// Skinlib
|
||||
filter: {
|
||||
skin: '(Any Model)',
|
||||
|
|
@ -76,8 +72,8 @@
|
|||
redirecting: 'Redirecting...',
|
||||
|
||||
// Change Privacy
|
||||
setAsPrivate: 'Set as Private',
|
||||
setAsPublic: 'Set as Public',
|
||||
setAsPrivate: 'Set as private',
|
||||
setAsPublic: 'Set as public',
|
||||
setPublicNotice: 'Sure to set this as public texture?',
|
||||
|
||||
deleteNotice: 'Are you sure to delete this texture?'
|
||||
|
|
@ -94,8 +90,8 @@
|
|||
removeItem: 'Remove from closet',
|
||||
setAsAvatar: 'Set as avatar',
|
||||
viewInSkinlib: 'View in skin library',
|
||||
switch2dPreview: 'Switch to 2D Preview',
|
||||
switch3dPreview: 'Switch to 3D Preview',
|
||||
switch2dPreview: 'Switch to 2D preview',
|
||||
switch3dPreview: 'Switch to 3D preview',
|
||||
removeFromClosetNotice: 'Sure to remove this texture from your closet?',
|
||||
emptySelectedPlayer: 'No player is selected.',
|
||||
emptySelectedTexture: 'No texture is selected.',
|
||||
|
|
@ -129,16 +125,17 @@
|
|||
unban: 'Unban',
|
||||
setAdmin: 'Set as admin',
|
||||
unsetAdmin: 'Remove admin',
|
||||
deleteUser: 'Delete User',
|
||||
deleteUser: 'Delete user',
|
||||
cannotDeleteAdmin: 'You can\'t delete admins.',
|
||||
cannotDeleteSuperAdmin: 'You can\'t delete super admin in this way',
|
||||
changeEmail: 'Edit Email',
|
||||
changeNickName: 'Edit Nickname',
|
||||
changePassword: 'Edit Password',
|
||||
cannotDeleteSuperAdmin: 'You can\'t delete super admins.',
|
||||
changeEmail: 'Edit email',
|
||||
changeNickName: 'Edit nickname',
|
||||
changePassword: 'Edit password',
|
||||
changeVerification: 'Switch verification status',
|
||||
newUserEmail: 'Please enter the new email:',
|
||||
newUserNickname: 'Please enter the new nickname:',
|
||||
newUserPassword: 'Please enter the new password:',
|
||||
deleteUserNotice: 'Are you sure to delete this user? It\' permanent.',
|
||||
deleteUserNotice: 'Are you sure to delete this user? It\'s permanent.',
|
||||
scoreTip: 'Press ENTER to submit new score',
|
||||
inspectHisOwner: 'Click to inspect the owner of this player',
|
||||
inspectHisPlayers: 'Click to inspect the players he owns',
|
||||
|
|
@ -149,6 +146,10 @@
|
|||
admin: 'Admin',
|
||||
superAdmin: 'Super Admin',
|
||||
|
||||
// Verification
|
||||
unverified: 'Unverified',
|
||||
verified: 'Verified',
|
||||
|
||||
// Players
|
||||
textureType: 'Texture Type',
|
||||
skin: 'Skin (:model Model)',
|
||||
|
|
@ -156,39 +157,49 @@
|
|||
pid: 'Texture ID',
|
||||
pidNotice: 'Please enter the tid of texture. Inputting 0 can clear texture of this player.',
|
||||
changePlayerTexture: 'Change textures of :player',
|
||||
changeTexture: 'Change Textures',
|
||||
changePlayerName: 'Change Player Name',
|
||||
changeOwner: 'Change Owner',
|
||||
changeTexture: 'Change textures',
|
||||
changePlayerName: 'Change player name',
|
||||
changeOwner: 'Change owner',
|
||||
deletePlayer: 'Delete',
|
||||
changePlayerOwner: 'Please enter the id of user which this player should be transferred to:',
|
||||
changePlayerOwner: 'Please enter the id of user which this player is transfering to:',
|
||||
deletePlayerNotice: 'Are you sure to delete this player? It\' permanent.',
|
||||
targetUser: 'Target user is :nickname',
|
||||
noSuchUser: 'No such user',
|
||||
noSuchUser: 'No such user.',
|
||||
changePlayerNameNotice: 'Please input new player name:',
|
||||
emptyPlayerName: 'Player name cannot be empty.',
|
||||
|
||||
// Plugins
|
||||
configurePlugin: 'Configure',
|
||||
noPluginConfigNotice: 'The plugin has been disabled or no configuration is provided.',
|
||||
deletePlugin: 'Delete',
|
||||
noDependencies: 'No Dependencies',
|
||||
whyDependencies: 'What\'s this?',
|
||||
statusEnabled: 'Enabled',
|
||||
statusDisabled: 'Disabled',
|
||||
pluginTitle: 'Plugin',
|
||||
pluginAuthor: 'Author',
|
||||
pluginVersion: 'Version',
|
||||
pluginName: 'Name',
|
||||
pluginOperations: 'Operations',
|
||||
pluginDescription: 'Description',
|
||||
pluginDependencies: 'Dependencies',
|
||||
pluginEnabled: 'Enabled',
|
||||
enablePlugin: 'Enable',
|
||||
disablePlugin: 'Disable',
|
||||
configurePlugin: 'Configure',
|
||||
installPlugin: 'Install',
|
||||
pluginInstalling: 'Installing...',
|
||||
updatePlugin: 'Update',
|
||||
pluginUpdating: 'Updating...',
|
||||
confirmUpdate: 'Are you sure to update ":plugin" from :old to :new?',
|
||||
deletePlugin: 'Delete',
|
||||
confirmDeletion: 'Are you sure to delete this plugin?',
|
||||
noDependencies: 'No Dependencies',
|
||||
whyDependencies: 'What\'s this?',
|
||||
noDependenciesNotice: 'There is no dependency definition in the plugin. It means that the plugin may be not compatible with the current version of Blessing Skin, and enabling it may cause unexpected problems. Do you really want to enable the plugin?',
|
||||
|
||||
// Update
|
||||
preparing: 'Preparing',
|
||||
downloadCompleted: 'Update package download completed.',
|
||||
extracting: 'Extracting update package..'
|
||||
extracting: 'Extracting update package...'
|
||||
},
|
||||
general: {
|
||||
skin: 'Skin',
|
||||
cape: 'Cape',
|
||||
fatalError: 'Fatal Error (Please contact the author)',
|
||||
fatalError: 'Fatal Error <small>(Please read the <a target="_blank" href="https://github.com/printempw/blessing-skin-server/wiki/FAQ">FAQ</a> before asking questions)</small>',
|
||||
confirmLogout: 'Sure to log out?',
|
||||
confirm: 'OK',
|
||||
cancel: 'Cancel',
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
option-saved: Option Saved.
|
||||
option-saved: Option saved.
|
||||
|
||||
homepage:
|
||||
title: Homepage
|
||||
|
||||
home_pic_url:
|
||||
title: Picture URL at Homepage
|
||||
hint: Path relative to homepage or full URL, leave empty to use default image
|
||||
hint: Path relative to homepage or full URL, leave empty to use default image.
|
||||
favicon_url:
|
||||
title: Website Icon
|
||||
hint: Path relative to resources/assets/ or full URL.
|
||||
description: The given image must have same width and height (leave blank to use default icon).
|
||||
copyright_prefer:
|
||||
title: Program Copyright
|
||||
description: Any evil modification applied on the footer program copyright (including deleting, modifying author, changing link target) with out permission is <b>FORBIDDEN</b>. The author reserves the right to pursue relevant responsibilities.
|
||||
description: "You can specify a different style of program copyright for each language. To edit a specific language's corresponding program copyright style, please switch to that language and submit your edit. <br><b>Warning:</b> Any evil modification applied on the footer program copyright (including deleting, modifying author, changing link target) with out permission is <b>FORBIDDEN</b>. The author reserves the right to pursue relevant responsibilities."
|
||||
copyright_text:
|
||||
title: Custom Copyright Text
|
||||
description: Placeholders are available in custom copyright text. e.g. <code>{site_name}</code> & <code>{site_url}</code>. You can also specify a different footer for each language. To edit a specific language's corresponding footer, please switch to that language and submit your edit.
|
||||
|
|
@ -27,7 +27,7 @@ customJsCss:
|
|||
custom_js: JavaScript
|
||||
|
||||
rate:
|
||||
title: About Scores
|
||||
title: Scores
|
||||
|
||||
score_per_storage:
|
||||
title: Storage
|
||||
|
|
@ -71,19 +71,25 @@ general:
|
|||
description: You can also specify a different site name and description for each language. To edit a specific language's corresponding site name or description text, please switch to that language and submit your edit.
|
||||
site_url:
|
||||
title: Site URL
|
||||
hint: Begin with http(s)://, nerver ends with slash
|
||||
hint: Begin with http(s)://, nerver ends with slash.
|
||||
user_can_register:
|
||||
title: Open Registration
|
||||
label: Everyone is allowed to register.
|
||||
register_with_player_name:
|
||||
title: Register with Player Name
|
||||
label: Require Minecraft's player name when user register
|
||||
require_verification:
|
||||
title: Account Verification
|
||||
label: Users must verify their email address first.
|
||||
regs_per_ip: Max accounts of one IP
|
||||
ip_get_method:
|
||||
title: Get IP via
|
||||
HTTP_X_FORWARDED_FOR: HTTP_X_FORWARDED_FOR (can be fabricated)
|
||||
REMOTE_ADDR: REMOTE_ADDR (isn't suit for sites under load balancer)
|
||||
hint: We have no method to get the real IP address of client with PHP.
|
||||
REMOTE_ADDR: REMOTE_ADDR (NOT suitable for sites under load balancer)
|
||||
hint: Unfortunately, we have no method to get the accurate client IP address with pure PHP.
|
||||
max_upload_file_size:
|
||||
title: Max Upload Size
|
||||
hint: "Limit of PHP in php.ini: :size"
|
||||
hint: "Limit specified in php.ini: :size"
|
||||
player_name_rule:
|
||||
title: Player Name Rule
|
||||
official: Letters, numbers and underscores (Mojang's official rule)
|
||||
|
|
@ -103,13 +109,20 @@ general:
|
|||
title: Invalid Textures
|
||||
label: Delete invalid textures automatically.
|
||||
hint: Delete textures records whose file no longer exists from skinlib.
|
||||
allow_downloading_texture:
|
||||
title: Downloading Textures
|
||||
label: Allow users to directly download the source file of a skinlib item.
|
||||
texture_name_regexp:
|
||||
title: Texture Name Rules
|
||||
hint: The RegExp for validating name of uploaded textures. Leave empty to allow any character except single, double quote and backslash.
|
||||
placeholder: Regular Expressions
|
||||
comment_script:
|
||||
title: Comment Script
|
||||
description: Placeholders are available in comment scripts. <code>{tid}</code> will be replaced with texture id, <code>{name}</code> will be replaced with texture name, <code>{url}</code> will be replaced with current URL.
|
||||
description: Placeholder is available, <code>{tid}</code> will be replaced with texture id, <code>{name}</code> will be replaced with texture name, <code>{url}</code> will be replaced with current URL.
|
||||
allow_sending_statistics:
|
||||
title: Statistics
|
||||
label: Send usage statistics anonymously.
|
||||
hint: Information about privacy will nerver be sent.
|
||||
hint: Privacy information will nerver be sent.
|
||||
|
||||
announ:
|
||||
title: Announcement
|
||||
|
|
@ -119,30 +132,20 @@ announ:
|
|||
|
||||
resources:
|
||||
title: Resource Files
|
||||
hint: Please adjust these options when CDN cache is on
|
||||
hint: Please check these options if you enabled CDN for your site.
|
||||
|
||||
force_ssl:
|
||||
title: Force SSL
|
||||
label: Use HTTPS protocol to load resources forcely.
|
||||
hint: Check SSL available before turning on
|
||||
label: Use HTTPS protocol to load all front-end assets.
|
||||
hint: Please check if SSL really available before turning on.
|
||||
auto_detect_asset_url:
|
||||
title: Assets URL
|
||||
label: Determine assets url automatically.
|
||||
description: Load asset files according to current URL. The site url will be used if this is not enabled. Please unable this if requests don't go through CDN.
|
||||
return_200_when_notfound:
|
||||
description: Please unable this if assets URLs are wrongly generated under a CDN. The site url will be used if this is not enabled.
|
||||
return_204_when_notfound:
|
||||
title: HTTP Response Code
|
||||
label: Return 200 instead of 404 when requesting un-existent player.
|
||||
description: If your CDN doesn't cache 404 pages, please turn this on. A flood of requests to un-existent players will greatly slow down the site.
|
||||
label: Return 204 instead of 404 when requesting non-existent player.
|
||||
description: If your CDN doesn't cache 404 pages, please turn this on. A flood of requests to non-existent players will greatly slow down the site.
|
||||
cache_expire_time:
|
||||
title: Cache Exipre Time
|
||||
hint: In seconds, 86400 = one day, 31536000 = one year
|
||||
|
||||
update:
|
||||
title: Check Update
|
||||
|
||||
check_update:
|
||||
title: Check Update
|
||||
label: Check update automatically and notify me.
|
||||
update_source:
|
||||
title: Update Source
|
||||
description: 'Available update source list can be found at: <a href="https://github.com/printempw/blessing-skin-server/wiki/%E6%9B%B4%E6%96%B0%E6%BA%90%E5%88%97%E8%A1%A8">@GitHub Wiki</a>'
|
||||
hint: In seconds, 86400 = one day, 31536000 = one year.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
database:
|
||||
connection-error: "Unable to connect to the target :type database, please check your configuration. The server replied with: :msg"
|
||||
connection-success: Connect to the target :type database [:server] successfully, just click NEXT to start installation.
|
||||
table-already-exists: There are some tables already exist in target database, which names conflict with ones we are going to create. To avoid data loss, please manually delete these tables :tables, or set a different table prefix.
|
||||
table-already-exists: There are some tables already exist in target database, whose names conflict with ones we are going to create. To avoid data loss, please manually delete these tables :tables, or set a different table prefix.
|
||||
|
||||
file:
|
||||
permission-error: Unable to create textures folder, please check the directory permissions or place one manually.
|
||||
no-dot-env: Unable to find environment configuration file. Please rename .env.example to .env (please refer to setup manual).
|
||||
dot-env-no-read-permission: Unable to read .env configuration file. Please check the file permission.
|
||||
|
||||
permissions:
|
||||
storage: Unable to write to storage directory, please check the permissions.
|
||||
|
|
@ -15,7 +16,7 @@ disabled-functions:
|
|||
|
||||
locked:
|
||||
title: Already installed
|
||||
text: You appear to have already installed Blessing Skin Server. To reinstall please clear your old database tables first, or use a new database table prefix.
|
||||
text: It appears that you have already installed Blessing Skin Server. To reinstall, please delete the tables in your database first, or use a new table prefix.
|
||||
button: Back to homepage
|
||||
|
||||
updates:
|
||||
|
|
@ -25,7 +26,7 @@ updates:
|
|||
welcome:
|
||||
title: One more step
|
||||
text: |
|
||||
Welcome to update to Blessing Skin Server v:version!
|
||||
Welcome! You are going to update to Blessing Skin Server v:version.
|
||||
We need to apply some updates to your database, click NEXT to continue.
|
||||
button: Next
|
||||
|
||||
|
|
@ -46,7 +47,7 @@ wizard:
|
|||
info:
|
||||
title: Information needed
|
||||
button: Run install
|
||||
text: To proceed with the installation fill this form with the details of the initial admin account. Don't worry, you can always change these settings later.
|
||||
text: To proceed with the installation, please fill this form with the details of the initial admin account. Don't worry, you can always change these settings later.
|
||||
|
||||
admin-email: Admin Email
|
||||
admin-notice: This is the <b>UNIQUE</b> super admin account who can GIVE or CANCEL other users' admin privilege.
|
||||
|
|
@ -54,10 +55,14 @@ wizard:
|
|||
pwd-notice: <b>Attention:</b> You will need the password to log in. Please keep it at a secure place.
|
||||
confirm-pwd: Confirm password
|
||||
site-name: Site name
|
||||
site-name-notice: This will be shown at title bar and homepage.
|
||||
site-name-notice: This will be shown on every page.
|
||||
secure: Security
|
||||
secure-notice: Generate random APP_KEY and SALT to make your site secured.
|
||||
|
||||
finish:
|
||||
title: Installation complete
|
||||
text: Blessing Skin Server has been installed. Thank you, and enjoy!
|
||||
|
||||
integrity-check:
|
||||
title: We could not complete the installation.
|
||||
description: The automatic upgrade script failed to update your database. <a target="_blank" href="https://github.com/printempw/blessing-skin-server/wiki/%E6%89%8B%E5%8A%A8%E5%AE%89%E8%A3%85-Blessing-Skin#-%E5%8D%87%E7%BA%A7%E8%87%B3-bs-v350">Learn more</a>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
general:
|
||||
filter: Filter
|
||||
my-upload: Uploaded by Me
|
||||
my-upload: Uploaded by me
|
||||
sort: Sort
|
||||
search-textures: Search For Textures
|
||||
upload-new-skin: Upload New Skin
|
||||
search-textures: Search for textures...
|
||||
upload-new-skin: Upload new skin
|
||||
no-result: No result.
|
||||
|
||||
filter:
|
||||
|
|
@ -13,17 +13,18 @@ filter:
|
|||
any-model: (Any Model)
|
||||
steve-model: (Steve Model)
|
||||
alex-model: (Alex Model)
|
||||
uploader: User (:name) Uploaded
|
||||
clean-filter: Clean Filter
|
||||
uploader: User (:name) uploaded
|
||||
clean-filter: Clean filter
|
||||
|
||||
sort:
|
||||
newest-uploaded: Newestly Uploaded
|
||||
most-likes: Most Likes
|
||||
newest-uploaded: Newestly uploaded
|
||||
most-likes: Most likes
|
||||
|
||||
item:
|
||||
steve: (Steve)
|
||||
alex: (Alex)
|
||||
cape: (Cape)
|
||||
apply: Quick apply
|
||||
remove-from-closet: Remove from closet
|
||||
add-to-closet: Add to closet
|
||||
anonymous: Please login first
|
||||
|
|
@ -31,27 +32,26 @@ item:
|
|||
|
||||
show:
|
||||
title: Texture Details
|
||||
anonymous: You must login to use closets
|
||||
likes: People who like this
|
||||
anonymous: You must login to use closets.
|
||||
likes: People who liked this
|
||||
|
||||
detail: Details
|
||||
name: Texture Name
|
||||
edit-name: Edit Name
|
||||
edit: Edit
|
||||
model: Applicable Model
|
||||
download-raw: Click to download raw texture
|
||||
size: File Size
|
||||
uploader: Uploader
|
||||
upload-at: Upload At
|
||||
|
||||
manage-panel: Manage Panel
|
||||
delete-texture: Delete Texture
|
||||
|
||||
notice: The texture which was deleted or setted to private will be removed from the closet of everyone who had favorited it.
|
||||
notice-admin: You are able to delete this texture or make it private. The operations will make it removed from the closet of everyone who had favorited it.
|
||||
manage-panel:
|
||||
title: Manage Panel
|
||||
notice: You can delete this texture or make it private. The operation will also remove it from the closet of everyone who had favorited it.
|
||||
|
||||
comment: Comment
|
||||
comment-not-available: Comment is not available.
|
||||
|
||||
delete-texture: Delete texture
|
||||
deleted: The requested texture was already deleted.
|
||||
contact-admin: Please contact the admins to remove this entry.
|
||||
private: The requested texture is private and only visible to the uploader and admins.
|
||||
|
|
@ -61,17 +61,18 @@ upload:
|
|||
|
||||
texture-name: Texture Name
|
||||
name-rule: Less than 32 characters and must not contain any special one.
|
||||
name-rule-regexp: Custom name rules are applied. :regexp
|
||||
texture-type: Texture Type
|
||||
select-file: Select File
|
||||
private-score-notice: It will spend you more scores to set it as private. You will be charged :score scores for every KB storage.
|
||||
private-score-notice: It will spend you more scores for setting it as private. You will be charged :score scores for per KB storage.
|
||||
privacy-notice: Prevent it from being visible at skin library.
|
||||
set-as-private: Make it Private
|
||||
set-as-private: Make it private
|
||||
|
||||
button: Upload
|
||||
|
||||
type-error: Incorrect mime type of uploaded file.
|
||||
invalid-size: Invalid :type file (Width :width, Height :height)
|
||||
invalid-hd-skin: Invalid HD skin (Width and height can not be devided by 32)
|
||||
invalid-size: Invalid :type file (width :width, height :height)
|
||||
invalid-hd-skin: Invalid HD skin (width and height should be divisible by 32)
|
||||
|
||||
lack-score: You don't have enough score to upload this texture.
|
||||
repeated: The texture is already uploaded by someone else. You can add it to your closet directly.
|
||||
|
|
@ -81,13 +82,17 @@ delete:
|
|||
success: The texture was deleted successfully.
|
||||
|
||||
privacy:
|
||||
change-privacy: Change Privacy
|
||||
set-as-private: Set as Private
|
||||
set-as-public: Set as Public
|
||||
success: The texture was setted to :privacy successfully.
|
||||
change-privacy: Change privacy
|
||||
set-as-private: Set as private
|
||||
set-as-public: Set as public
|
||||
success: The texture was set to :privacy successfully.
|
||||
|
||||
rename:
|
||||
success: The texture was renamed to :name successfully.
|
||||
|
||||
no-permission: You aren't the uploader of this texture.
|
||||
non-existent: Non-existent texture.
|
||||
model:
|
||||
success: The texture's model was changed to :model successfully.
|
||||
duplicate: "The same texture available for the chosen model already exists in skinlib (TID: :tid). You can add it to your closet directly."
|
||||
|
||||
no-permission: You have no permission to moderate this texture.
|
||||
non-existent: No such texture.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,23 @@ last-sign: Last signed at :time
|
|||
sign-remain-time: Available after :time :unit
|
||||
announcement: Announcement
|
||||
|
||||
verification:
|
||||
disabled: Email verification is not available.
|
||||
frequent-mail: You click the send button too fast. Wait for 60 secs, guy.
|
||||
verified: Your account is already verified.
|
||||
success: Verification link was sent, please check your inbox.
|
||||
failed: We failed to send you the verification link. Detailed message :msg
|
||||
notice:
|
||||
title: Verify Your Account
|
||||
message: You must verify your email address before using the skin hosting service. Haven't received the email?
|
||||
resend: Click here to send again.
|
||||
sending: Sending...
|
||||
mail:
|
||||
title: Verify Your Account on :sitename
|
||||
message: You are receiving this email because someone registered an account with this email address on :sitename.
|
||||
reset: 'Click here to verify your account: <a href=":url">:url</a>'
|
||||
ignore: If you did not register an account, no further action is required.
|
||||
|
||||
score-intro:
|
||||
title: What is score?
|
||||
introduction: |
|
||||
|
|
@ -43,10 +60,10 @@ closet:
|
|||
reset: Clear selected
|
||||
title: Which player should be applied to?
|
||||
empty: It seems that you own no player...
|
||||
add: Add new player
|
||||
add: Add a new player
|
||||
|
||||
add:
|
||||
success: Added :name to closet successfully~
|
||||
success: Added :name to closet successfully.
|
||||
repeated: You have already added this texture.
|
||||
not-found: We cannot find this texture.
|
||||
lack-score: You don't have enough score to add it to closet.
|
||||
|
|
@ -57,7 +74,7 @@ closet:
|
|||
|
||||
remove:
|
||||
title: Remove from closet
|
||||
success: Texture was removed from closet successfully.
|
||||
success: The texture was removed from closet successfully.
|
||||
non-existent: The texture does not exist in your closet.
|
||||
|
||||
player:
|
||||
|
|
@ -79,9 +96,6 @@ player:
|
|||
cape: Cape:
|
||||
empty: Nothing
|
||||
|
||||
pname-rule: Could only contain letters, numbers and dashes.
|
||||
pname-rule-chinese: Could only contain chinese characters, letters, numbers and dashes.
|
||||
|
||||
player-name-rule:
|
||||
official: Player name may only contains letters, numbers and underscores.
|
||||
cjk: Player name may contains letters, numbers, underscores and CJK Unified Ideographs.
|
||||
|
|
@ -115,22 +129,22 @@ player:
|
|||
profile:
|
||||
avatar:
|
||||
title: Change Avatar?
|
||||
notice: Click the gear icon「<i class="fa fa-cog"></i>」of any skin in your skinlib, then click 「Set as avatar」. We will cut the head segment of that skin for you. If there is no icon like this, please unable the extensions like ADBlock.
|
||||
wrong-type: You can't set a cape as avatar
|
||||
success: Avatar setted successfully
|
||||
notice: Click the gear icon "<i class="fa fa-cog"></i>" of any skin in your closet, then click "Set as avatar". We will cut the head segment of that skin for you. If there is no icon like this, please try to unable your ADs blocking extension.
|
||||
wrong-type: You can't set a cape as avatar.
|
||||
success: New avatar was set successfully.
|
||||
|
||||
password:
|
||||
title: Change Password
|
||||
old: Old Password
|
||||
new: New Password
|
||||
confirm: Repeat Password
|
||||
button: Change Password
|
||||
wrong-password: Original password is not correct.
|
||||
button: Change password
|
||||
wrong-password: Wrong original password.
|
||||
success: Password updated successfully, please log in again.
|
||||
|
||||
nickname:
|
||||
title: Change Nickname
|
||||
empty: No nickname is setted now.
|
||||
empty: No nickname is set now.
|
||||
rule: Whatever you like expect special characters
|
||||
success: Nickname is successfully updated to :nickname
|
||||
|
||||
|
|
@ -138,16 +152,16 @@ profile:
|
|||
title: Change Email
|
||||
new: New Email
|
||||
password: Current Password
|
||||
button: Change Email
|
||||
button: Change email
|
||||
wrong-password: Wrong password.
|
||||
existed: This email address is used.
|
||||
existed: This email address is occupied.
|
||||
success: Email address updated successfully, please log in again.
|
||||
|
||||
delete:
|
||||
title: Delete Account
|
||||
notice: Sure to delete your account on :site?
|
||||
admin: Admin account can not be deleted.
|
||||
button: Delete My Account
|
||||
button: Delete my account
|
||||
|
||||
modal-title: You need to enter your password to continue
|
||||
modal-notice: |
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
# Blessing Skin
|
||||
username: ':attribute format is invalid.'
|
||||
player_name: 'The :attribute contains invalid character.'
|
||||
no_special_chars: 'The :attribute must not contain special characters.'
|
||||
preference: 'The :attribute must be default or slim.'
|
||||
model: 'The :attribute must be steve, alex or cape.'
|
||||
username: ':attribute format is invalid.'
|
||||
player_name: 'The :attribute contains invalid character.'
|
||||
texture_name_regexp: 'The :attribute contains invalid character.'
|
||||
no_special_chars: 'The :attribute must not contain special characters.'
|
||||
preference: 'The :attribute must be default or slim.'
|
||||
model: 'The :attribute must be steve, alex or cape.'
|
||||
|
||||
accepted: 'The :attribute must be accepted.'
|
||||
active_url: 'The :attribute is not a valid URL.'
|
||||
|
|
@ -14,10 +15,10 @@ alpha_num: 'The :attribute may only contain letters and numbers.'
|
|||
array: 'The :attribute must be an array.'
|
||||
before: 'The :attribute must be a date before :date.'
|
||||
between:
|
||||
numeric: 'The :attribute must be between :min and :max.'
|
||||
file: 'The :attribute must be between :min and :max kilobytes.'
|
||||
string: 'The :attribute must be between :min and :max characters.'
|
||||
array: 'The :attribute must have between :min and :max items.'
|
||||
numeric: 'The :attribute must be between :min and :max.'
|
||||
file: 'The :attribute must be between :min and :max kilobytes.'
|
||||
string: 'The :attribute must be between :min and :max characters.'
|
||||
array: 'The :attribute must have between :min and :max items.'
|
||||
boolean: 'The :attribute field must be true or false.'
|
||||
confirmed: 'The :attribute confirmation does not match.'
|
||||
date: 'The :attribute is not a valid date.'
|
||||
|
|
@ -36,16 +37,16 @@ integer: 'The :attribute must be an integer.'
|
|||
ip: 'The :attribute must be a valid IP address.'
|
||||
json: 'The :attribute must be a valid JSON string.'
|
||||
max:
|
||||
numeric: 'The :attribute may not be greater than :max.'
|
||||
file: 'The :attribute may not be greater than :max kilobytes.'
|
||||
string: 'The :attribute may not be greater than :max characters.'
|
||||
array: 'The :attribute may not have more than :max items.'
|
||||
numeric: 'The :attribute may not be greater than :max.'
|
||||
file: 'The :attribute may not be greater than :max kilobytes.'
|
||||
string: 'The :attribute may not be greater than :max characters.'
|
||||
array: 'The :attribute may not have more than :max items.'
|
||||
mimes: 'The :attribute must be a file of type: :values.'
|
||||
min:
|
||||
numeric: 'The :attribute must be at least :min.'
|
||||
file: 'The :attribute must be at least :min kilobytes.'
|
||||
string: 'The :attribute must be at least :min characters.'
|
||||
array: 'The :attribute must have at least :min items.'
|
||||
numeric: 'The :attribute must be at least :min.'
|
||||
file: 'The :attribute must be at least :min kilobytes.'
|
||||
string: 'The :attribute must be at least :min characters.'
|
||||
array: 'The :attribute must have at least :min items.'
|
||||
not_in: 'The selected :attribute is invalid.'
|
||||
numeric: 'The :attribute must be a number.'
|
||||
present: 'The :attribute field must be present.'
|
||||
|
|
@ -59,10 +60,10 @@ required_without: 'The :attribute field is required when :values is not present.
|
|||
required_without_all: 'The :attribute field is required when none of :values are present.'
|
||||
same: 'The :attribute and :other must match.'
|
||||
size:
|
||||
numeric: 'The :attribute must be :size.'
|
||||
file: 'The :attribute must be :size kilobytes.'
|
||||
string: 'The :attribute must be :size characters.'
|
||||
array: 'The :attribute must contain :size items.'
|
||||
numeric: 'The :attribute must be :size.'
|
||||
file: 'The :attribute must be :size kilobytes.'
|
||||
string: 'The :attribute must be :size characters.'
|
||||
array: 'The :attribute must contain :size items.'
|
||||
string: 'The :attribute must be a string.'
|
||||
timezone: 'The :attribute must be a valid zone.'
|
||||
unique: 'The :attribute has already been taken.'
|
||||
|
|
@ -75,8 +76,10 @@ url: 'The :attribute format is invalid.'
|
|||
# Here you may specify custom validation messages for attributes using the
|
||||
# convention "attribute.rule" to name the lines. This makes it quick to
|
||||
# specify a specific custom language line for a given attribute rule.
|
||||
#
|
||||
custom:
|
||||
attribute-name: { rule-name: custom-message }
|
||||
attribute-name:
|
||||
rule-name: custom-message
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Custom Validation Attributes
|
||||
|
|
@ -87,9 +90,7 @@ custom:
|
|||
# of "email". This simply helps us make messages a little cleaner.
|
||||
#
|
||||
attributes:
|
||||
file: File
|
||||
name: Name
|
||||
player_name: Player Name
|
||||
new_player_name: Player Name
|
||||
identification: Email or player name
|
||||
site name: Site Name
|
||||
player_name: player name
|
||||
new_player_name: player name
|
||||
identification: email or player name
|
||||
sitename: site name
|
||||
|
|
|
|||
2
resources/lang/overrides/.gitignore
vendored
Normal file
2
resources/lang/overrides/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -16,15 +16,19 @@ users:
|
|||
banned: 封禁
|
||||
admin: 管理员
|
||||
super-admin: 超级管理员
|
||||
verification:
|
||||
title: 邮箱验证
|
||||
operations:
|
||||
title: 更多操作
|
||||
non-existent: 用户不存在
|
||||
no-permission: 你无权操作此用户
|
||||
invalid: 无效 action
|
||||
invalid: 无效的 action
|
||||
email:
|
||||
change: 修改邮箱
|
||||
existed: :email 已被占用
|
||||
existed: 邮箱 :email 已被占用
|
||||
success: 邮箱修改成功
|
||||
verification:
|
||||
success: 用户的邮箱验证状态已修改
|
||||
nickname:
|
||||
change: 修改昵称
|
||||
success: 昵称已成功设置为 :new
|
||||
|
|
@ -52,8 +56,8 @@ users:
|
|||
delete:
|
||||
delete: 删除用户
|
||||
success: 账号已被成功删除
|
||||
cant-super-admin: 超级管理员账号不能被这样删除的啦
|
||||
cant-admin: 你不能删除管理员账号哦
|
||||
cant-super-admin: 超级管理员账号不能被删除
|
||||
cant-admin: 无法删除管理员账号
|
||||
|
||||
players:
|
||||
no-permission: 你无权操作此角色
|
||||
|
|
@ -86,27 +90,15 @@ customize:
|
|||
yellow-light: 黄色主题 - 白色侧边栏
|
||||
green: 绿色主题
|
||||
green-light: 绿色主题 - 白色侧边栏
|
||||
purple: 基佬紫
|
||||
purple: 紫色主题
|
||||
purple-light: 紫色主题 - 白色侧边栏
|
||||
red: 喜庆红(笑)
|
||||
red: 红色主题
|
||||
red-light: 红色主题 - 白色侧边栏
|
||||
black: 高端黑
|
||||
black: 黑色主题
|
||||
black-light: 黑色主题 - 白色侧边栏
|
||||
|
||||
plugins:
|
||||
name: 名称
|
||||
description: 描述
|
||||
author: 作者
|
||||
version: 版本
|
||||
dependencies: 依赖关系
|
||||
|
||||
status:
|
||||
title: 状态
|
||||
enabled: 已启用
|
||||
disabled: 已禁用
|
||||
|
||||
operations:
|
||||
title: 操作
|
||||
enabled: :plugin 已启用
|
||||
unsatisfied:
|
||||
notice: 无法启用此插件,因为其仍有未满足的依赖关系。请检查以下插件的版本,更新或安装它们:
|
||||
|
|
@ -117,6 +109,14 @@ plugins:
|
|||
no-config-notice: 插件未安装或未提供配置页面
|
||||
not-found: 插件不存在
|
||||
|
||||
market:
|
||||
connection-error: 无法连接至插件市场源(更换市场源请参考:http://t.cn/Rk6X37l),错误信息::error
|
||||
non-existent: 插件 :plugin 不存在
|
||||
download-failed: 插件下载失败,错误信息::error
|
||||
shasum-failed: 文件校验失败,请尝试重新下载
|
||||
unzip-failed: 插件解压缩失败,错误信息::error
|
||||
install-success: 插件安装成功
|
||||
|
||||
empty: 无结果
|
||||
|
||||
update:
|
||||
|
|
@ -151,7 +151,8 @@ update:
|
|||
title: 注意事项
|
||||
text: |
|
||||
请根据你的主机所在位置(国内/国外)选择更新源。
|
||||
如错选至相对于你的主机速度较慢的源,可能会造成检查/下载更新页面长时间无响应。
|
||||
如错选至相对于你的主机速度较慢的源,可能会造成检查与下载更新页面长时间无响应。
|
||||
如何更换更新源?<a target="_blank" href="https://github.com/printempw/blessing-skin-server/wiki/%E6%9B%B4%E6%96%B0%E6%BA%90%E5%88%97%E8%A1%A8">点击了解详情</a>。
|
||||
|
||||
download:
|
||||
downloading: 正在下载更新包
|
||||
|
|
@ -159,9 +160,9 @@ update:
|
|||
|
||||
errors:
|
||||
prefix: 发生错误:
|
||||
connection: 无法访问当前更新源。详细信息:
|
||||
write-permission: 您的服务器不支持自动更新:创建下载缓存文件夹失败,请检查目录权限。
|
||||
connection: 无法访问当前更新源。详细信息::error
|
||||
write-permission: 你的服务器不支持自动更新:创建下载缓存文件夹失败,请检查目录权限。
|
||||
unzip: 更新包解压缩失败。错误代码:
|
||||
overwrite: 您的服务器不支持自动更新:无法覆盖文件。
|
||||
overwrite: 你的服务器不支持自动更新:无法覆盖文件。
|
||||
|
||||
invalid-action: 无效的操作名
|
||||
|
|
|
|||
|
|
@ -1,50 +1,51 @@
|
|||
login:
|
||||
title: 登录
|
||||
button: 登录
|
||||
message: 登录以管理您的角色及皮肤
|
||||
message: 登录以管理你的角色与皮肤
|
||||
keep: 保持登录状态
|
||||
success: 登录成功,欢迎回来~
|
||||
success: 登录成功,欢迎回来
|
||||
|
||||
check:
|
||||
anonymous: 非法访问,请先登录
|
||||
admin: 看起来你并不是管理员哦
|
||||
banned: 你已经被本站封禁啦,请联系管理员解决
|
||||
token: 无效的 token,请重新登录
|
||||
anonymous: 未授权的访问,请先登录
|
||||
verified: 你必须验证邮箱后才能访问此页面
|
||||
admin: 只有管理员才能访问此页面
|
||||
super-admin: 只有超级管理员才能访问此页面
|
||||
banned: 你已被本站封禁,详情请联系站点管理员
|
||||
token: 登录状态已过期,请重新登录
|
||||
|
||||
register:
|
||||
title: 注册
|
||||
button: 注册
|
||||
message: 欢迎使用 :sitename!
|
||||
player-name-intro: 游戏内的角色名,注册后可修改
|
||||
nickname-intro: 昵称可使用汉字,不可包含特殊字符
|
||||
repeat-pwd: 重复密码
|
||||
close: 残念。。本皮肤站已经关闭注册咯 QAQ
|
||||
success: 注册成功,正在跳转~
|
||||
max: 你最多只能注册 :regs 个账户哦
|
||||
registered: 这个邮箱已经注册过啦,换一个吧
|
||||
close: 本站已关闭注册
|
||||
success: 注册成功,正在跳转...
|
||||
max: 你在本站注册的账号已达到上限 :regs 个,无法继续注册
|
||||
registered: 此邮箱已被占用
|
||||
|
||||
forgot:
|
||||
title: 忘记密码
|
||||
button: 发送
|
||||
message: 我们将会向您发送一封验证邮件
|
||||
login-link: 我又想起来了
|
||||
close: 本站已关闭重置密码功能
|
||||
frequent-mail: 你邮件发送得太频繁啦,过 60 秒后再点发送吧
|
||||
disabled: 本站已关闭重置密码功能
|
||||
frequent-mail: 你邮件发送得太频繁啦,过会儿再点发送吧
|
||||
unregistered: 该邮箱尚未注册
|
||||
|
||||
mail:
|
||||
title: 重置您在 :sitename 上的账户密码
|
||||
success: 邮件已发送,一小时内有效,请注意查收。
|
||||
failed: 邮件发送失败,详细信息::msg
|
||||
message: 您收到这封邮件,是因为在 :sitename 的用户重置密码功能使用了您的地址。
|
||||
ignore: 如果您并没有访问过我们的网站,或没有进行上述操作,请忽略这封邮件。 您不需要退订或进行其他进一步的操作。
|
||||
reset: 重置密码
|
||||
notice: 本邮件由系统自动发送,就算你回复了我们也不会回复你哦
|
||||
mail:
|
||||
title: 重置您在 :sitename 上的账户密码
|
||||
message: 您收到这封邮件,是因为有人在 :sitename 的密码重置功能中使用了您的地址。
|
||||
reset: 点击此链接重置您的密码:<a href=":url">:url</a>
|
||||
ignore: 如果您并没有访问过我们的网站,或没有进行上述操作,请忽略这封邮件。
|
||||
|
||||
reset:
|
||||
title: 重置密码
|
||||
button: 重置
|
||||
invalid: 无效的链接
|
||||
expired: 链接已过期
|
||||
expired: 链接已失效,请重新发送验证邮件
|
||||
message: :username,在这里重置你的密码
|
||||
success: 密码重置成功
|
||||
|
||||
|
|
@ -52,27 +53,35 @@ bind:
|
|||
title: 绑定邮箱
|
||||
button: 绑定
|
||||
message: 你需要绑定邮箱地址以继续使用本站
|
||||
introduction: 邮箱地址仅用于重置密码,我们将不会向您发送任何垃圾邮件
|
||||
registered: 该邮箱已被占用
|
||||
introduction: 邮箱地址仅用于重置密码,我们不会向您发送任何垃圾邮件
|
||||
registered: 此邮箱已被占用
|
||||
|
||||
verify:
|
||||
title: 邮箱验证
|
||||
success: 邮箱验证成功
|
||||
message: 欢迎使用 :sitename!
|
||||
button: 返回首页
|
||||
invalid: 无效的链接
|
||||
expired: 链接已失效,请重新发送验证邮件
|
||||
|
||||
validation:
|
||||
identification: 邮箱或角色名格式错误
|
||||
email: 邮箱格式错误
|
||||
captcha: 验证码填写错误
|
||||
user: 用户不存在哦
|
||||
password: 密码不对哦~
|
||||
user: 用户不存在
|
||||
password: 密码错误
|
||||
|
||||
logout:
|
||||
success: 登出成功~
|
||||
fail: 并没有有效的 session
|
||||
success: 登出成功
|
||||
fail: 未找到已保存的登录信息
|
||||
|
||||
nickname: 昵称
|
||||
player-name: 游戏内角色名
|
||||
email: Email
|
||||
identification: Email 或角色名
|
||||
password: 密码
|
||||
captcha: 请输入验证码
|
||||
change-captcha: 点击以更换图片
|
||||
login-link: 已经有账号了?登录
|
||||
login-link: 已有账号?点击登录
|
||||
forgot-link: 忘记密码?
|
||||
register-link: 注册新账号
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
http:
|
||||
msg-403: 你并没有权限查看此页面
|
||||
msg-403: 你没有权限查看此页面
|
||||
msg-404: 这里啥都没有哦
|
||||
msg-500: 内部服务器错误,请稍后再试
|
||||
msg-503: 网站维护中
|
||||
method-not-allowed: 不允许的 HTTP 请求方法
|
||||
csrf-token-mismatch: Token 不正确,请尝试刷新页面
|
||||
|
||||
general:
|
||||
title: 出现错误
|
||||
|
|
@ -10,9 +13,12 @@ exception:
|
|||
code: '错误码::code'
|
||||
detail: '详细信息::msg'
|
||||
message: |
|
||||
如果您是访客,这说明网站程序出现了一些错误,请您稍后再试或联系站长。
|
||||
如果您是站长,请开启 .env 中的 APP_DEBUG 以查看详细信息。
|
||||
如果你是访客,这说明网站程序出现了一些错误,请稍后再试或者联系站长。
|
||||
如果你是站长,那么请开启 .env 中的 APP_DEBUG 查看详细信息。
|
||||
|
||||
plugins:
|
||||
duplicate: 【插件定义重复】:dir1 目录下的插件与 :dir2 目录下的插件使用了相同的 name 定义并造成了冲突。请检查您的插件目录,移除其中一个插件或者使用不同的 name 属性。
|
||||
directory: 配置文件 .env 中指定的插件加载目录(PLUGINS_DIR)不存在或无法打开,请检查您的配置。错误信息::msg
|
||||
duplicate: 【插件定义重复】:dir1 目录下的插件与 :dir2 目录下的插件使用了相同的 name 定义并造成了冲突。请检查你的插件目录,移除其中一个插件或者使用不同的 name 属性。
|
||||
directory: 配置文件 .env 中指定的插件加载目录(PLUGINS_DIR)不存在或无法打开,请检查你的配置。错误信息::msg
|
||||
|
||||
cipher:
|
||||
unsupported: 不支持的密码加密方式 `:cipher`,请检查你的 .env 配置文件
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ plugin-market: 插件市场
|
|||
plugin-configs: 插件配置
|
||||
customize: 个性化
|
||||
options: 站点配置
|
||||
import-v2: 导入数据
|
||||
score-options: 积分配置
|
||||
check-update: 检查更新
|
||||
download-update: 下载更新
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
features:
|
||||
multi-player:
|
||||
first:
|
||||
icon: fa-users
|
||||
name: 多角色
|
||||
desc: 一个账户可绑定多个游戏角色
|
||||
|
||||
sharing:
|
||||
second:
|
||||
icon: fa-share-alt
|
||||
name: 分享
|
||||
desc: 浏览皮肤库,添加喜爱的皮肤并与好友分享
|
||||
|
||||
free:
|
||||
third:
|
||||
icon: fa-cloud
|
||||
name: 永久免费
|
||||
desc: 我们承诺永不收取任何费用
|
||||
|
||||
introduction: :sitename 提供 Minecraft 角色皮肤的上传以及托管服务。配合 CustomSkinLoader 等换肤 MOD,您可以为您的游戏角色设置皮肤与披风,并让其他玩家在游戏中看到。
|
||||
introduction: :sitename 提供 Minecraft 角色皮肤的上传以及托管服务。配合 CustomSkinLoader 等换肤 MOD,你可以为你的游戏角色设置皮肤与披风,并让其他玩家在游戏中看到。
|
||||
|
||||
start: 开始使用
|
||||
|
|
|
|||
|
|
@ -1,32 +1,24 @@
|
|||
/*!
|
||||
* Blessing Skin Chinese Translations
|
||||
*
|
||||
* @see https://github.com/printempw/blessing-skin-server
|
||||
* @author printempw <h@prinzeugen.net>
|
||||
*
|
||||
* NOTE: this file must be saved in UTF-8 encoding.
|
||||
*/
|
||||
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
$.locales['zh_CN'] = {
|
||||
auth: {
|
||||
// Login
|
||||
emptyIdentification: '你还没有填写邮箱/角色名哦',
|
||||
emptyPassword: '密码要好好填哦',
|
||||
emptyCaptcha: '你还没有填写验证码哦',
|
||||
emptyIdentification: '邮箱/角色名不能为空',
|
||||
emptyPassword: '密码不能为空',
|
||||
emptyCaptcha: '验证码不能为空',
|
||||
login: '登录',
|
||||
loggingIn: '登录中',
|
||||
tooManyFails: '你尝试的次数太多啦,请输入验证码',
|
||||
|
||||
// Register
|
||||
emptyEmail: '你还没有填写邮箱哦',
|
||||
invalidEmail: '邮箱格式不正确!',
|
||||
invalidPassword: '无效的密码。密码长度应该大于 8 并小于 32。',
|
||||
emptyEmail: '邮箱不能为空',
|
||||
invalidEmail: '邮箱格式不正确',
|
||||
invalidPassword: '密码长度应该大于 8 并小于 32 个字符',
|
||||
emptyConfirmPwd: '确认密码不能为空',
|
||||
invalidConfirmPwd: '密码和确认的密码不一样诶?',
|
||||
emptyNickname: '你还没有填写昵称哦',
|
||||
invalidConfirmPwd: '确认密码不一致',
|
||||
emptyNickname: '昵称不能为空',
|
||||
emptyPlayerName: '角色名不能为空',
|
||||
register: '注册',
|
||||
registering: '注册中',
|
||||
|
||||
|
|
@ -40,8 +32,9 @@
|
|||
// Like
|
||||
addToCloset: '添加至衣柜',
|
||||
removeFromCloset: '从衣柜中移除',
|
||||
setItemName: '给你的皮肤起个名字吧~',
|
||||
emptyItemName: '你还没有填写要收藏的材质名称啊',
|
||||
setItemName: '为收藏的衣柜物品命名:',
|
||||
applyNotice: '收藏后可以在我的衣柜里将皮肤应用至角色',
|
||||
emptyItemName: '收藏的衣柜物品名称不能为空',
|
||||
anonymous: '请先登录',
|
||||
private: '私密',
|
||||
|
||||
|
|
@ -60,19 +53,22 @@
|
|||
},
|
||||
|
||||
// Preview
|
||||
badSkinSize: '所选皮肤文件的尺寸不对哦',
|
||||
badCapeSize: '所选披风文件的尺寸不对哦',
|
||||
badSkinSize: '所选的皮肤文件尺寸不符合要求',
|
||||
badCapeSize: '所选的披风文件尺寸不符合要求',
|
||||
|
||||
// Rename
|
||||
setNewTextureName: '请输入新的材质名称:',
|
||||
emptyNewTextureName: '你还没有输入新名称啊',
|
||||
emptyNewTextureName: '材质名称不能为空',
|
||||
|
||||
// Change Model
|
||||
setNewTextureModel: '请选择新的材质适用模型:',
|
||||
|
||||
// Upload
|
||||
emptyTextureName: '给你的材质起个名字吧',
|
||||
emptyTextureName: '材质名称不能为空',
|
||||
emptyTextureType: '请选择材质的类型',
|
||||
emptyUploadFile: '你还没有上传任何文件哦',
|
||||
encodingError: '错误:这张图片编码不对哦',
|
||||
fileExtError: '错误:皮肤文件必须为 PNG 格式',
|
||||
emptyUploadFile: '你还没有选择任何文件',
|
||||
encodingError: '错误:图片文件编码不符合要求',
|
||||
fileExtError: '错误:材质文件必须为 PNG 格式',
|
||||
upload: '确认上传',
|
||||
uploading: '上传中',
|
||||
redirecting: '正在跳转...',
|
||||
|
|
@ -91,7 +87,7 @@
|
|||
timeUnitMin: '分钟',
|
||||
|
||||
// Closet
|
||||
emptyClosetMsg: '<p>衣柜里啥都没有哦~</p><p>去<a href=":url">皮肤库</a>看看吧~</p>',
|
||||
emptyClosetMsg: '<p>衣柜里什么都没有哦……</p><p>去<a href=":url">皮肤库</a>看看吧</p>',
|
||||
renameItem: '重命名物品',
|
||||
removeItem: '从衣柜中移除',
|
||||
setAsAvatar: '设为头像',
|
||||
|
|
@ -99,27 +95,27 @@
|
|||
switch2dPreview: '切换 2D 预览',
|
||||
switch3dPreview: '切换 3D 预览',
|
||||
removeFromClosetNotice: '确定要从衣柜中移除此材质吗?',
|
||||
emptySelectedPlayer: '你还没有选择角色哦',
|
||||
emptySelectedTexture: '你还没有选择要应用的材质哦',
|
||||
emptySelectedPlayer: '请选择要应用选中材质的角色',
|
||||
emptySelectedTexture: '请选择要应用的材质',
|
||||
renameClosetItem: '请输入此衣柜物品的新名称:',
|
||||
|
||||
// Player
|
||||
changePlayerName: '请输入角色名:',
|
||||
emptyPlayerName: '你还没有填写名称哦',
|
||||
emptyPlayerName: '角色名不能为空',
|
||||
clearTexture: '确定要重置该用户的皮肤/披风吗?',
|
||||
deletePlayer: '真的要删除该玩家吗?',
|
||||
deletePlayer: '真的要删除该角色吗?',
|
||||
deletePlayerNotice: '这将是永久性的删除',
|
||||
chooseClearTexture: '选择要删除的材质类型',
|
||||
noClearChoice: '您还没选择要删除的材质类型',
|
||||
noClearChoice: '请选择要删除的材质类型',
|
||||
|
||||
// Profile
|
||||
setAvatar: '确定要将此材质设置为用户头像吗?',
|
||||
setAvatarNotice: '将会自动截取皮肤头部',
|
||||
emptyNewNickName: '你还没有填写新昵称啊',
|
||||
emptyNewNickName: '昵称不能为空',
|
||||
changeNickName: '确定要将昵称设置为 :new_nickname 吗?',
|
||||
emptyPassword: '原密码不能为空',
|
||||
emptyNewPassword: '新密码要好好填哦',
|
||||
emptyNewEmail: '你还没有填写新邮箱啊',
|
||||
emptyNewPassword: '新密码不能为空',
|
||||
emptyNewEmail: '邮箱不能为空',
|
||||
changeEmail: '确定要将用户邮箱更改为 :new_email 吗?',
|
||||
emptyDeletePassword: '请先输入当前用户密码'
|
||||
},
|
||||
|
|
@ -132,11 +128,12 @@
|
|||
setAdmin: '设为管理员',
|
||||
unsetAdmin: '解除管理员',
|
||||
deleteUser: '删除用户',
|
||||
cannotDeleteAdmin: '你不能删除管理员账号哦',
|
||||
cannotDeleteSuperAdmin: '超级管理员账号不能被这样删除的啦',
|
||||
cannotDeleteAdmin: '你没有权限删除管理员账号',
|
||||
cannotDeleteSuperAdmin: '超级管理员账号不能被删除',
|
||||
changeEmail: '修改邮箱',
|
||||
changeNickName: '修改昵称',
|
||||
changePassword: '更改密码',
|
||||
changeVerification: '修改邮箱验证状态',
|
||||
newUserEmail: '请输入新邮箱:',
|
||||
newUserNickname: '请输入新昵称:',
|
||||
newUserPassword: '请输入新密码:',
|
||||
|
|
@ -151,6 +148,10 @@
|
|||
admin: '管理员',
|
||||
superAdmin: '超级管理员',
|
||||
|
||||
// Verification
|
||||
unverified: '未验证',
|
||||
verified: '已验证',
|
||||
|
||||
// Players
|
||||
textureType: '材质类型',
|
||||
skin: '皮肤(:model 模型)',
|
||||
|
|
@ -165,21 +166,31 @@
|
|||
changePlayerOwner: '请输入此角色要让渡至的用户 UID:',
|
||||
deletePlayerNotice: '真的要删除此角色吗?此操作不可恢复',
|
||||
targetUser: '目标用户::nickname',
|
||||
noSuchUser: '没有这个用户哦~',
|
||||
noSuchUser: '目标用户不存在',
|
||||
changePlayerNameNotice: '请输入新的角色名:',
|
||||
emptyPlayerName: '您还没填写角色名呢',
|
||||
emptyPlayerName: '角色名不能为空',
|
||||
|
||||
// Plugins
|
||||
configurePlugin: '插件配置',
|
||||
noPluginConfigNotice: '插件已被禁用或无配置页',
|
||||
deletePlugin: '删除插件',
|
||||
pluginTitle: '插件',
|
||||
pluginAuthor: '作者',
|
||||
pluginVersion: '版本',
|
||||
pluginName: '插件标识',
|
||||
pluginOperations: '操作',
|
||||
pluginDescription: '描述',
|
||||
pluginDependencies: '依赖关系',
|
||||
pluginEnabled: '已启用',
|
||||
enablePlugin: '启用',
|
||||
disablePlugin: '禁用',
|
||||
configurePlugin: '配置',
|
||||
installPlugin: '安装',
|
||||
pluginInstalling: '正在安装...',
|
||||
updatePlugin: '更新',
|
||||
pluginUpdating: '正在更新...',
|
||||
confirmUpdate: '确定将「:plugin」从 :old 升级至 :new?',
|
||||
deletePlugin: '删除',
|
||||
confirmDeletion: '真的要删除这个插件吗?',
|
||||
noDependencies: '无要求',
|
||||
whyDependencies: '为什么会这样?',
|
||||
statusEnabled: '已启用',
|
||||
statusDisabled: '已禁用',
|
||||
enablePlugin: '启用插件',
|
||||
disablePlugin: '禁用插件',
|
||||
confirmDeletion: '真的要删除这个插件吗?',
|
||||
noDependenciesNotice: '此插件没有声明任何依赖关系,这代表它有可能并不兼容此版本的 Blessing Skin,请将此插件升级至可能的最新版本。强行启用可能导致无法预料的后果。你确定要启用此插件吗?',
|
||||
|
||||
// Update
|
||||
|
|
@ -190,7 +201,7 @@
|
|||
general: {
|
||||
skin: '皮肤',
|
||||
cape: '披风',
|
||||
fatalError: '严重错误(请联系作者)',
|
||||
fatalError: '严重错误<small>(提问前请先查阅 <a target="_blank" href="https://github.com/printempw/blessing-skin-server/wiki/FAQ">常见问题</a>)</small>',
|
||||
confirmLogout: '确定要登出吗?',
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
|
|
@ -263,7 +274,7 @@
|
|||
uploadBatch: '批量上传',
|
||||
uploadExtra: '表单数据上传'
|
||||
},
|
||||
dropZoneTitle: '拖拽文件到这里 …<br>支持多文件同时上传',
|
||||
dropZoneTitle: '拖拽文件到这里 …',
|
||||
dropZoneClickTitle: '<br>(或点击{files}按钮选择文件)',
|
||||
fileActionSettings: {
|
||||
removeTitle: '删除文件',
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ homepage:
|
|||
description: 所使用的图像必须具有相同的宽度和高度(留空以使用默认图标)
|
||||
copyright_prefer:
|
||||
title: 程序版权信息
|
||||
description: 对于任何恶意修改页面<b>右下角</b>的版权信息(包括不限于删除、修改作者信息、修改链接指向)的用户,作者保留对其追究责任的权力。
|
||||
description: 每种支持的语言都可以对应不同的程序版权信息,如果想要编辑某种特定语言下的版权信息,请在右上角切换至该语言后再提交修改。<b>对于任何恶意修改页面右下角的版权信息(包括不限于删除、修改作者信息、修改链接指向)的用户,作者保留对其追究责任的权利。</b>
|
||||
copyright_text:
|
||||
title: 自定义版权文字
|
||||
description: 自定义版权文字内可使用占位符,<code>{site_name}</code> 将会被自动替换为站点名称,<code>{site_url}</code> 会被替换为站点地址。每种支持的语言都可以对应不同的自定义版权文字,如果想要编辑某种特定语言下的版权文字,请在右上角切换至该语言后再提交修改。
|
||||
|
|
@ -75,6 +75,12 @@ general:
|
|||
user_can_register:
|
||||
title: 开放注册
|
||||
label: 任何人都可以注册
|
||||
register_with_player_name:
|
||||
title: 使用角色名注册
|
||||
label: 注册时要求填写游戏内角色名
|
||||
require_verification:
|
||||
title: 邮箱验证
|
||||
label: 用户必须验证邮箱后才能使用皮肤托管等功能
|
||||
regs_per_ip: 每个 IP 限制注册数
|
||||
ip_get_method:
|
||||
title: IP 获取方式
|
||||
|
|
@ -103,6 +109,13 @@ general:
|
|||
title: 失效材质
|
||||
label: 自动删除失效材质
|
||||
hint: 自动从皮肤库中删除文件不存在的材质记录
|
||||
allow_downloading_texture:
|
||||
title: 直接下载材质
|
||||
label: 允许用户直接下载皮肤库中材质的原始文件
|
||||
texture_name_regexp:
|
||||
title: 材质名称规则
|
||||
hint: 皮肤库上传材质时名称的正则表达式。留空表示允许使用除半角单双引号、反斜杠以外的任意字符。
|
||||
placeholder: 正则表达式,不懂别乱填
|
||||
comment_script:
|
||||
title: 评论代码
|
||||
description: 评论代码内可使用占位符,<code>{tid}</code> 将会被自动替换为材质的 id,<code>{name}</code> 会被替换为材质名称,<code>{url}</code> 会被替换为当前页面地址。
|
||||
|
|
@ -129,20 +142,10 @@ resources:
|
|||
title: 资源地址
|
||||
label: 自动判断资源文件地址
|
||||
description: 根据当前 URL 自动加载资源文件,如果关闭则将根据「站点地址」填写的内容加载。如果出现 CDN 回源问题请关闭
|
||||
return_200_when_notfound:
|
||||
return_204_when_notfound:
|
||||
title: HTTP 响应码
|
||||
label: 请求不存在的角色时返回 200 而不是 404
|
||||
label: 请求不存在的角色时返回 204 而不是 404
|
||||
description: 如果你的 CDN 不缓存 404 页面,请打开此项。否则大量对不存在角色的 Profile 请求会加重站点负载。
|
||||
cache_expire_time:
|
||||
title: 缓存失效时间
|
||||
hint: 秒数,86400 = 一天,31536000 = 一年
|
||||
|
||||
update:
|
||||
title: 更新选项
|
||||
|
||||
check_update:
|
||||
title: 检查更新
|
||||
label: 自动检查更新并提示
|
||||
update_source:
|
||||
title: 更新源
|
||||
description: 可用的更新源列表可以在这里查看:<a href="https://github.com/printempw/blessing-skin-server/wiki/%E6%9B%B4%E6%96%B0%E6%BA%90%E5%88%97%E8%A1%A8">@GitHub Wiki</a>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
database:
|
||||
connection-error: 无法连接至 :type 服务器,请检查你的配置。服务器返回的信息:「:msg」
|
||||
connection-error: 无法连接至 :type 服务器,请检查你的配置。服务器返回的信息::msg
|
||||
connection-success: 成功连接至 :type 服务器 [:server] ,点击下一步以开始安装。
|
||||
table-already-exists: 检测到目标数据库中已存在如下数据表 :tables,它们与本程序即将创建的数据表名称冲突,为了避免原有数据被覆盖,请手动删除它们,或者为本程序选择一个不同的数据表前缀。
|
||||
|
||||
file:
|
||||
permission-error: textures 文件夹创建失败,请确认目录权限是否正确,或者手动放置一个。
|
||||
no-dot-env: 找不到配置文件,请将 .env.example 重命名至 .env 并仔细阅读安装指南。
|
||||
dot-env-no-read-permission: 无法读取 .env 配置文件,请检查文件权限。
|
||||
|
||||
permissions:
|
||||
storage: 无法写入 storage 目录,请检查目录权限是否正确
|
||||
|
|
@ -26,7 +27,7 @@ updates:
|
|||
title: 还差一小步
|
||||
text: |
|
||||
欢迎升级至 Blessing Skin Server v:version!
|
||||
我们需要升级您的数据库,点击下一步以继续。
|
||||
我们需要升级你的数据库,点击下一步以继续。
|
||||
button: 下一步
|
||||
|
||||
success:
|
||||
|
|
@ -46,12 +47,12 @@ wizard:
|
|||
info:
|
||||
title: 填写信息
|
||||
button: 开始安装
|
||||
text: 您需要填写一些基本信息。无需担心填错,这些信息以后可以再次修改。
|
||||
text: 你需要填写一些基本信息。无需担心填错,这些信息以后可以再次修改。
|
||||
|
||||
admin-email: 管理员邮箱
|
||||
admin-notice: 这是<b>唯一</b>的超级管理员账号,可 添加/取消 其他管理员。
|
||||
admin-notice: 这是<b>唯一</b>的超级管理员账号,可添加或移除其他管理员。
|
||||
password: 密码
|
||||
pwd-notice: <b>重要:</b>您将需要此密码来登录管理皮肤站,请将其保存在安全的位置。
|
||||
pwd-notice: <b>重要:</b>你将需要此密码来登录管理皮肤站,请将其保存在安全的位置。
|
||||
confirm-pwd: 重复密码
|
||||
site-name: 站点名称
|
||||
site-name-notice: 将会显示在首页以及标题栏
|
||||
|
|
@ -60,4 +61,8 @@ wizard:
|
|||
|
||||
finish:
|
||||
title: 安装成功!
|
||||
text: Blessing Skin Server 安装完成。您是否还沉浸在愉悦的安装过程中?很遗憾,一切皆已完成! :)
|
||||
text: Blessing Skin Server 安装完成。你是否还沉浸在愉悦的安装过程中?很遗憾,一切皆已完成! :)
|
||||
|
||||
integrity-check:
|
||||
title: 安装不完全
|
||||
description: 由于某些神秘的原因,我们无法自动完成数据库的更新。<a target="_blank" href="https://github.com/printempw/blessing-skin-server/wiki/%E6%89%8B%E5%8A%A8%E5%AE%89%E8%A3%85-Blessing-Skin#-%E5%8D%87%E7%BA%A7%E8%87%B3-bs-v350">了解详情</a>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ item:
|
|||
steve: (Steve)
|
||||
alex: (Alex)
|
||||
cape: (披风)
|
||||
apply: 立即使用
|
||||
remove-from-closet: 从衣柜中移除
|
||||
add-to-closet: 添加至衣柜
|
||||
anonymous: 请先登录
|
||||
|
|
@ -36,30 +37,31 @@ show:
|
|||
|
||||
detail: 详细信息
|
||||
name: 名称
|
||||
edit-name: 修改名称
|
||||
edit: 修改
|
||||
model: 适用模型
|
||||
download-raw: 右键另存为即可下载原始皮肤文件
|
||||
size: 文件大小
|
||||
uploader: 上传者
|
||||
upload-at: 上传日期
|
||||
|
||||
manage-panel: 管理面板
|
||||
delete-texture: 删除材质
|
||||
notice: 材质设为隐私或被删除后将会从每一个收藏者的衣柜中移除。
|
||||
notice-admin: 你可以将此材质设为隐私或删除。这将会使此材质从每一个收藏者的衣柜中移除。
|
||||
manage-panel:
|
||||
title: 管理面板
|
||||
notice: 你可以将此材质设为隐私或删除。这将会使此材质从每一个收藏者的衣柜中移除。
|
||||
|
||||
comment: 评论区
|
||||
comment-not-available: 本站未开启评论服务
|
||||
|
||||
delete-texture: 删除材质
|
||||
deleted: 请求的材质文件已经被删除
|
||||
contact-admin: 请联系管理员删除该条目
|
||||
private: 请求的材质已经设为隐私,仅上传者和管理员可查看
|
||||
private: 请求的材质已经设为私密,仅上传者和管理员可查看
|
||||
|
||||
upload:
|
||||
title: 上传材质
|
||||
|
||||
texture-name: 材质名称
|
||||
name-rule: 材质名称应该小于 32 个字节且不能包含奇怪的符号
|
||||
name-rule-regexp: 本站已应用特殊的名称规则::regexp
|
||||
texture-type: 材质类型
|
||||
select-file: 选择文件
|
||||
private-score-notice: 私密材质将会消耗更多的积分:每 KB 存储空间 :score 积分
|
||||
|
|
@ -68,11 +70,11 @@ upload:
|
|||
|
||||
button: 确认上传
|
||||
|
||||
type-error: 文件格式不对哦
|
||||
type-error: 文件格式不符合要求,请检查你的材质文件
|
||||
invalid-size: 不是有效的 :type 文件(宽 :width,高 :height)
|
||||
invalid-hd-skin: 不是有效的高清皮肤(宽和高不是 32 的整数倍)
|
||||
|
||||
lack-score: 积分不够啦
|
||||
lack-score: 积分不足
|
||||
repeated: 已经有人上传过这个材质了,直接添加到衣柜使用吧~
|
||||
success: 材质 :name 上传成功
|
||||
|
||||
|
|
@ -88,5 +90,9 @@ privacy:
|
|||
rename:
|
||||
success: 材质名称已被成功设置为 :name
|
||||
|
||||
no-permission: 你不是这个材质的上传者哦
|
||||
model:
|
||||
success: 材质的适用模型已被修改为 :model
|
||||
duplicate: 已经有人上传过适用于该模型的相同材质了,直接去皮肤库收藏使用吧(TID::tid)
|
||||
|
||||
no-permission: 你没有权限修改此材质
|
||||
non-existent: 材质不存在
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user