blessing-skin-server/app/Http/Controllers/SkinlibController.php

440 lines
15 KiB
PHP
Raw Normal View History

2016-07-21 22:01:57 +08:00
<?php
2016-08-28 10:05:21 +08:00
namespace App\Http\Controllers;
2016-07-21 22:01:57 +08:00
use App\Models\Texture;
2019-12-14 11:10:37 +08:00
use App\Models\User;
use Auth;
use Blessing\Filter;
2020-06-04 16:36:10 +08:00
use Blessing\Rejection;
use Illuminate\Contracts\Events\Dispatcher;
2020-03-24 18:05:46 +08:00
use Illuminate\Database\Eloquent\Builder;
2020-06-04 16:36:10 +08:00
use Illuminate\Filesystem\FilesystemAdapter;
2019-12-14 11:10:37 +08:00
use Illuminate\Http\Request;
2020-06-11 10:22:52 +08:00
use Illuminate\Http\UploadedFile;
2020-06-04 16:36:10 +08:00
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
2020-08-20 10:28:27 +08:00
use League\CommonMark\GithubFlavoredMarkdownConverter;
2019-12-14 11:10:37 +08:00
use Storage;
2016-07-21 22:01:57 +08:00
class SkinlibController extends Controller
2016-07-21 22:01:57 +08:00
{
2020-06-05 23:35:49 +08:00
public function __construct()
{
$this->middleware(function (Request $request, $next) {
/** @var User */
$user = $request->user();
/** @var Texture */
$texture = $request->route('texture');
if ($texture->uploader != $user->uid && !$user->isAdmin()) {
return json(trans('skinlib.no-permission'), 1)
->setStatusCode(403);
}
return $next($request);
})->only(['rename', 'privacy', 'type', 'delete']);
2020-08-20 08:53:43 +08:00
$this->middleware(function (Request $request, $next) {
/** @var User */
$user = $request->user();
/** @var Texture */
$texture = $request->route('texture');
if (!$texture->public) {
if (!Auth::check() || ($user->uid != $texture->uploader && !$user->isAdmin())) {
$statusCode = (int) option('status_code_for_private');
if ($statusCode === 404) {
abort($statusCode, trans('skinlib.show.deleted'));
} else {
abort(403, trans('skinlib.show.private'));
}
}
}
return $next($request);
})->only(['show', 'info']);
2020-06-05 23:35:49 +08:00
}
2020-03-24 18:05:46 +08:00
public function library(Request $request)
{
2019-03-14 23:55:49 +08:00
$user = Auth::user();
2016-07-26 13:36:24 +08:00
// Available filters: skin, steve, alex, cape
2020-03-24 18:05:46 +08:00
$type = $request->input('filter', 'skin');
$uploader = $request->input('uploader');
$keyword = $request->input('keyword');
2019-03-19 15:19:33 +08:00
$sort = $request->input('sort', 'time');
$sortBy = $sort == 'time' ? 'upload_at' : $sort;
2020-03-24 18:05:46 +08:00
return Texture::orderBy($sortBy, 'desc')
2020-10-14 09:48:45 +08:00
->when(
$type === 'skin',
fn (Builder $query) => $query->whereIn('type', ['steve', 'alex']),
fn (Builder $query) => $query->where('type', $type),
)
->when($keyword, fn (Builder $query, $keyword) => $query->like('name', $keyword))
->when($uploader, fn (Builder $query, $uploader) => $query->where('uploader', $uploader))
2020-03-24 18:05:46 +08:00
->when($user, function (Builder $query, User $user) {
if (!$user->isAdmin()) {
2020-04-27 18:46:22 +08:00
// use closure-style `where` clause to lift up SQL priority
return $query->where(function (Builder $query) use ($user) {
$query
->where('public', true)
->orWhere('uploader', $user->uid);
});
2020-03-24 18:05:46 +08:00
}
}, function (Builder $query) {
// show public textures only to anonymous visitors
return $query->where('public', true);
})
->join('users', 'uid', 'uploader')
->select(['tid', 'name', 'type', 'uploader', 'public', 'likes', 'nickname'])
->paginate(20);
2016-07-21 22:01:57 +08:00
}
2020-08-20 08:48:53 +08:00
public function show(Filter $filter, Texture $texture)
2016-07-21 22:01:57 +08:00
{
2020-06-04 16:42:10 +08:00
/** @var User */
$user = Auth::user();
2020-06-04 16:42:10 +08:00
/** @var FilesystemAdapter */
$disk = Storage::disk('textures');
2020-08-20 08:48:53 +08:00
if ($disk->missing($texture->hash)) {
if (option('auto_del_invalid_texture')) {
2020-08-20 08:48:53 +08:00
$texture->delete();
}
2020-08-19 17:58:31 +08:00
abort(404, trans('skinlib.show.deleted'));
2016-08-16 22:58:21 +08:00
}
$badges = [];
2020-04-01 10:07:34 +08:00
$uploader = $texture->owner;
if ($uploader) {
if ($uploader->isAdmin()) {
$badges[] = ['text' => 'STAFF', 'color' => 'primary'];
}
$badges = $filter->apply('user_badges', $badges, [$uploader]);
}
2019-12-16 10:49:09 +08:00
$grid = [
'layout' => [
['md-8', 'md-4'],
],
'widgets' => [
[
['shared.previewer'],
2020-03-20 16:19:18 +08:00
['skinlib.widgets.show.side'],
2019-12-16 10:49:09 +08:00
],
],
];
$grid = $filter->apply('grid:skinlib.show', $grid);
return view('skinlib.show')
->with('texture', $texture)
2019-12-16 10:49:09 +08:00
->with('grid', $grid)
2019-03-23 19:52:14 +08:00
->with('extra', [
'download' => option('allow_downloading_texture'),
'currentUid' => $user ? $user->uid : 0,
'admin' => $user && $user->isAdmin(),
'inCloset' => $user && $user->closet()->where('tid', $texture->tid)->count() > 0,
2020-04-01 10:07:34 +08:00
'uploaderExists' => (bool) $uploader,
'nickname' => optional($uploader)->nickname ?? trans('general.unexistent-user'),
'report' => intval(option('reporter_score_modification', 0)),
'badges' => $badges,
2019-03-23 19:52:14 +08:00
]);
2016-07-21 22:01:57 +08:00
}
2020-06-04 18:12:58 +08:00
public function info(Texture $texture)
2016-07-21 22:01:57 +08:00
{
2020-06-04 18:12:58 +08:00
return $texture;
2016-07-21 22:01:57 +08:00
}
2019-12-16 11:02:39 +08:00
public function upload(Filter $filter)
2016-07-21 22:01:57 +08:00
{
2019-12-16 11:02:39 +08:00
$grid = [
'layout' => [
['md-6', 'md-6'],
],
'widgets' => [
[
['skinlib.widgets.upload.input'],
['shared.previewer'],
],
],
];
$grid = $filter->apply('grid:skinlib.upload', $grid);
2020-08-20 10:28:27 +08:00
$converter = new GithubFlavoredMarkdownConverter();
2019-12-25 15:48:34 +08:00
return view('skinlib.upload')
2019-12-16 11:02:39 +08:00
->with('grid', $grid)
2019-03-23 19:52:14 +08:00
->with('extra', [
'rule' => ($regexp = option('texture_name_regexp'))
? trans('skinlib.upload.name-rule-regexp', compact('regexp'))
: trans('skinlib.upload.name-rule'),
'privacyNotice' => trans(
'skinlib.upload.private-score-notice',
['score' => option('private_score_per_storage')]
),
'score' => (int) auth()->user()->score,
'scorePublic' => (int) option('score_per_storage'),
'scorePrivate' => (int) option('private_score_per_storage'),
'closetItemCost' => (int) option('score_per_closet_item'),
'award' => (int) option('score_award_per_texture'),
2020-08-20 10:28:27 +08:00
'contentPolicy' => $converter->convertToHtml(option_localized('content_policy')),
2019-09-18 23:06:48 +08:00
]);
2016-07-21 22:01:57 +08:00
}
2020-06-04 16:36:10 +08:00
public function handleUpload(
Request $request,
Filter $filter,
Dispatcher $dispatcher
) {
$file = $request->file('file');
if ($file && !$file->isValid()) {
Log::error($file->getErrorMessage());
}
2016-07-21 22:01:57 +08:00
2020-06-04 16:36:10 +08:00
$data = $request->validate([
'name' => [
'required',
option('texture_name_regexp') ? 'regex:'.option('texture_name_regexp') : 'string',
],
'file' => 'required|mimes:png|max:'.option('max_upload_file_size'),
'type' => ['required', Rule::in(['steve', 'alex', 'cape'])],
'public' => 'required|boolean',
]);
2020-06-11 10:22:52 +08:00
/** @var UploadedFile */
2020-06-04 16:36:10 +08:00
$file = $filter->apply('uploaded_texture_file', $file);
$name = $data['name'];
$name = $filter->apply('uploaded_texture_name', $name, [$file]);
$can = $filter->apply('can_upload_texture', true, [$file, $name]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
2019-04-04 11:16:04 +08:00
}
2020-06-04 16:36:10 +08:00
$type = $data['type'];
$size = getimagesize($file);
$ratio = $size[0] / $size[1];
if ($type == 'steve' || $type == 'alex') {
if ($ratio != 2 && $ratio != 1) {
$message = trans('skinlib.upload.invalid-size', [
'type' => trans('general.skin'),
'width' => $size[0],
'height' => $size[1],
]);
2016-07-21 22:01:57 +08:00
2020-06-04 16:36:10 +08:00
return json($message, 1);
}
if ($size[0] % 64 != 0 || $size[1] % 32 != 0) {
$message = trans('skinlib.upload.invalid-hd-skin', [
'type' => trans('general.skin'),
'width' => $size[0],
'height' => $size[1],
]);
2020-06-04 16:36:10 +08:00
return json($message, 1);
}
} elseif ($type == 'cape') {
if ($ratio != 2) {
$message = trans('skinlib.upload.invalid-size', [
'type' => trans('general.cape'),
'width' => $size[0],
'height' => $size[1],
]);
return json($message, 1);
}
}
2016-07-21 22:01:57 +08:00
2020-06-04 16:36:10 +08:00
$hash = hash_file('sha256', $file);
$hash = $filter->apply('uploaded_texture_hash', $hash, [$file]);
/** @var User */
$user = Auth::user();
$duplicated = Texture::where('hash', $hash)
2020-10-14 09:48:45 +08:00
->where(
fn (Builder $query) => $query->where('public', true)->orWhere('uploader', $user->uid)
)
->first();
2020-06-04 16:36:10 +08:00
if ($duplicated) {
// if the texture already uploaded was set to private,
// then allow to re-upload it.
2020-06-04 16:36:10 +08:00
return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]);
2016-07-21 22:01:57 +08:00
}
2020-06-04 16:36:10 +08:00
$size = ceil($file->getSize() / 1024);
$isPublic = is_string($data['public'])
? $data['public'] === '1'
: $data['public'];
$cost = $size * (
$isPublic
? option('score_per_storage')
: option('private_score_per_storage')
);
$cost += option('score_per_closet_item');
$cost -= option('score_award_per_texture', 0);
if ($user->score < $cost) {
return json(trans('skinlib.upload.lack-score'), 1);
}
2020-06-04 16:36:10 +08:00
$dispatcher->dispatch('texture.uploading', [$file, $name, $hash]);
$texture = new Texture();
$texture->name = $name;
$texture->type = $type;
$texture->hash = $hash;
$texture->size = $size;
$texture->public = $isPublic;
$texture->uploader = $user->uid;
$texture->likes = 1;
$texture->save();
/** @var FilesystemAdapter */
$disk = Storage::disk('textures');
if ($disk->missing($hash)) {
2020-06-11 10:22:52 +08:00
$file->storePubliclyAs('', $hash, ['disk' => 'textures']);
2020-06-04 16:36:10 +08:00
}
2016-07-21 22:01:57 +08:00
2019-07-30 14:29:02 +08:00
$user->score -= $cost;
2020-06-04 16:36:10 +08:00
$user->closet()->attach($texture->tid, ['item_name' => $name]);
2019-07-30 14:29:02 +08:00
$user->save();
2020-06-04 16:36:10 +08:00
$dispatcher->dispatch('texture.uploaded', [$texture, $file]);
return json(trans('skinlib.upload.success', ['name' => $name]), 0, [
'tid' => $texture->tid,
2019-03-14 23:55:49 +08:00
]);
}
public function delete(Texture $texture, Dispatcher $dispatcher, Filter $filter)
2016-07-21 22:01:57 +08:00
{
$can = $filter->apply('can_delete_texture', true, [$texture]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
}
$dispatcher->dispatch('texture.deleting', [$texture]);
2016-07-24 15:56:23 +08:00
// check if file occupied
2020-06-05 23:35:49 +08:00
if (Texture::where('hash', $texture->hash)->count() === 1) {
Storage::disk('textures')->delete($texture->hash);
}
2016-07-21 22:01:57 +08:00
2019-05-07 15:16:53 +08:00
$texture->delete();
2019-05-19 13:49:44 +08:00
$dispatcher->dispatch('texture.deleted', [$texture]);
2019-05-07 15:16:53 +08:00
return json(trans('skinlib.delete.success'), 0);
}
public function privacy(Texture $texture, Dispatcher $dispatcher, Filter $filter)
2016-07-21 22:01:57 +08:00
{
$can = $filter->apply('can_update_texture_privacy', true, [$texture]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
}
$uploader = $texture->owner;
2020-06-05 23:35:49 +08:00
$score_diff = $texture->size
* (option('private_score_per_storage') - option('score_per_storage'))
* ($texture->public ? -1 : 1);
if ($texture->public && option('take_back_scores_after_deletion', true)) {
2019-03-20 23:28:04 +08:00
$score_diff -= option('score_award_per_texture', 0);
}
2019-03-23 11:06:36 +08:00
if ($uploader->score + $score_diff < 0) {
2017-09-18 19:28:38 +08:00
return json(trans('skinlib.upload.lack-score'), 1);
}
if (!$texture->public) {
$duplicated = Texture::where('hash', $texture->hash)
->where('public', true)
->first();
if ($duplicated) {
return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]);
}
}
$dispatcher->dispatch('texture.privacy.updating', [$texture]);
2019-07-30 14:29:02 +08:00
$uploader->score += $score_diff;
$uploader->save();
2017-04-21 18:44:11 +08:00
2020-06-05 23:35:49 +08:00
$texture->public = !$texture->public;
$texture->save();
$dispatcher->dispatch('texture.privacy.updated', [$texture]);
2020-06-05 23:35:49 +08:00
$message = trans('skinlib.privacy.success', [
'privacy' => (
$texture->public
? trans('general.public')
: trans('general.private')),
]);
2016-07-24 15:56:23 +08:00
2020-06-05 23:35:49 +08:00
return json($message, 0);
}
2016-07-24 15:56:23 +08:00
public function rename(
Request $request,
Dispatcher $dispatcher,
Filter $filter,
Texture $texture
) {
2020-06-05 23:35:49 +08:00
$data = $request->validate(['name' => [
'required',
option('texture_name_regexp')
? 'regex:'.option('texture_name_regexp')
: 'string',
]]);
$name = $data['name'];
2016-07-24 15:56:23 +08:00
$can = $filter->apply('can_update_texture_name', true, [$texture, $name]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
}
$dispatcher->dispatch('texture.name.updating', [$texture, $name]);
$old = $texture->replicate();
2020-06-05 23:35:49 +08:00
$texture->name = $name;
$texture->save();
2016-07-24 15:56:23 +08:00
$dispatcher->dispatch('texture.name.updated', [$texture, $old]);
2020-06-05 23:35:49 +08:00
return json(trans('skinlib.rename.success', ['name' => $name]), 0);
}
public function type(
Request $request,
Dispatcher $dispatcher,
Filter $filter,
Texture $texture
) {
2020-05-31 16:37:09 +08:00
$data = $request->validate([
2020-06-05 23:35:49 +08:00
'type' => ['required', Rule::in(['steve', 'alex', 'cape'])],
]);
2020-06-05 23:35:49 +08:00
$type = $data['type'];
$can = $filter->apply('can_update_texture_type', true, [$texture, $type]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
}
$dispatcher->dispatch('texture.type.updating', [$texture, $type]);
$old = $texture->replicate();
2020-06-05 23:35:49 +08:00
$texture->type = $type;
$texture->save();
$dispatcher->dispatch('texture.type.updated', [$texture, $old]);
2020-06-05 23:35:49 +08:00
return json(trans('skinlib.model.success', ['model' => $type]), 0);
}
2016-07-21 22:01:57 +08:00
}