feat: max texture width & texture sanitize (#662)
* feat: sanitize uploaded file when user upload texture * feat: limit max texture width to avoid png bomb * style: apply php-cs-fixer fixes * chore: set default value for max_texture_width option * Update skinlib.yml Co-authored-by: Pig Fang <g-plane@hotmail.com> --------- Co-authored-by: Pig Fang <g-plane@hotmail.com>
This commit is contained in:
parent
01fe3eb4cb
commit
761cbb7828
|
|
@ -163,6 +163,10 @@ class OptionsController extends Controller
|
|||
->text('max_upload_file_size')->addon('KB')
|
||||
->hint(trans('options.general.max_upload_file_size.hint', ['size' => ini_get('upload_max_filesize')]));
|
||||
|
||||
$form->group('max_texture_width')
|
||||
->text('max_texture_width')->addon('px')
|
||||
->hint(trans('options.general.max_texture_width.hint'));
|
||||
|
||||
$form->select('player_name_rule')
|
||||
->option('official', trans('options.general.player_name_rule.official'))
|
||||
->option('cjk', trans('options.general.player_name_rule.cjk'))
|
||||
|
|
|
|||
|
|
@ -220,6 +220,16 @@ class SkinlibController extends Controller
|
|||
$type = $data['type'];
|
||||
$size = getimagesize($file);
|
||||
|
||||
$maxWidth = option('max_texture_width', 8192);
|
||||
if ($size[0] > $maxWidth) {
|
||||
$message = trans('skinlib.upload.too-wide', [
|
||||
'width' => $size[0],
|
||||
'maxWidth' => $maxWidth,
|
||||
]);
|
||||
|
||||
return json($message, 1);
|
||||
}
|
||||
|
||||
if ($size[0] % 64 != 0 || $size[1] % 32 != 0) {
|
||||
$message = trans('skinlib.upload.invalid-size', [
|
||||
'type' => $type === 'cape' ? trans('general.cape') : trans('general.skin'),
|
||||
|
|
@ -253,8 +263,19 @@ class SkinlibController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
$hash = hash_file('sha256', $file);
|
||||
$hash = $filter->apply('uploaded_texture_hash', $hash, [$file]);
|
||||
$image = imagecreatefrompng($file);
|
||||
$imageSanitized = imagecreatetruecolor($size[0], $size[1]);
|
||||
imagealphablending($imageSanitized, false);
|
||||
imagesavealpha($imageSanitized, true);
|
||||
imagecopy($imageSanitized, $image, 0, 0, 0, 0, $size[0], $size[1]);
|
||||
|
||||
ob_start();
|
||||
imagepng($imageSanitized);
|
||||
$fileSanitized = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$hash = hash('sha256', $fileSanitized);
|
||||
$hash = $filter->apply('uploaded_texture_hash', $hash, [$fileSanitized]);
|
||||
|
||||
/** @var User */
|
||||
$user = Auth::user();
|
||||
|
|
@ -270,11 +291,11 @@ class SkinlibController extends Controller
|
|||
return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]);
|
||||
}
|
||||
|
||||
$size = ceil($file->getSize() / 1024);
|
||||
$fileSize = ceil(strlen($fileSanitized) / 1024);
|
||||
$isPublic = is_string($data['public'])
|
||||
? $data['public'] === '1'
|
||||
: $data['public'];
|
||||
$cost = $size * (
|
||||
$cost = $fileSize * (
|
||||
$isPublic
|
||||
? option('score_per_storage')
|
||||
: option('private_score_per_storage')
|
||||
|
|
@ -285,13 +306,13 @@ class SkinlibController extends Controller
|
|||
return json(trans('skinlib.upload.lack-score'), 1);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('texture.uploading', [$file, $name, $hash]);
|
||||
$dispatcher->dispatch('texture.uploading', [$fileSanitized, $name, $hash]);
|
||||
|
||||
$texture = new Texture();
|
||||
$texture->name = $name;
|
||||
$texture->type = $type;
|
||||
$texture->hash = $hash;
|
||||
$texture->size = $size;
|
||||
$texture->size = $fileSize;
|
||||
$texture->public = $isPublic;
|
||||
$texture->uploader = $user->uid;
|
||||
$texture->likes = 1;
|
||||
|
|
@ -300,14 +321,14 @@ class SkinlibController extends Controller
|
|||
/** @var FilesystemAdapter */
|
||||
$disk = Storage::disk('textures');
|
||||
if ($disk->missing($hash)) {
|
||||
$file->storePubliclyAs('', $hash, ['disk' => 'textures']);
|
||||
$disk->put($hash, $fileSanitized);
|
||||
}
|
||||
|
||||
$user->score -= $cost;
|
||||
$user->closet()->attach($texture->tid, ['item_name' => $name]);
|
||||
$user->save();
|
||||
|
||||
$dispatcher->dispatch('texture.uploaded', [$texture, $file]);
|
||||
$dispatcher->dispatch('texture.uploaded', [$texture, $fileSanitized]);
|
||||
|
||||
return json(trans('skinlib.upload.success', ['name' => $name]), 0, [
|
||||
'tid' => $texture->tid,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
use App\Services\Facades\Option;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Option::set('max_texture_width', 8192);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
}
|
||||
};
|
||||
|
|
@ -110,7 +110,10 @@ general:
|
|||
regs_per_ip: Max accounts of one IP
|
||||
max_upload_file_size:
|
||||
title: Max Upload Size
|
||||
hint: "Limit specified in php.ini: :size"
|
||||
hint: 'Limit specified in php.ini: :size'
|
||||
max_texture_width:
|
||||
title: Max Texture Width
|
||||
hint: Maximum width of uploaded textures, must be an integer multiple of 64
|
||||
player_name_rule:
|
||||
title: Player Name Rule
|
||||
official: Letters, numbers and underscores (Mojang's official rule)
|
||||
|
|
@ -129,8 +132,8 @@ general:
|
|||
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.
|
||||
title: Downloading Textures
|
||||
label: Allow users to directly download the source file of a skinlib item.
|
||||
status_code_for_private:
|
||||
title: HTTP Code for Rejecting Accessing Private Textures
|
||||
texture_name_regexp:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ upload:
|
|||
private-score-notice: It will spend you more scores for setting it as private. You will be charged :score scores for per KB storage.
|
||||
invalid-size: Invalid :type file (width :width, height :height)
|
||||
invalid-hd-skin: Invalid HD skin (width and height should be divisible by 32)
|
||||
too-wide: The texture is too wide (:widthpx). Maximum width allowed is :maxWidthpx
|
||||
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.
|
||||
success: Texture :name was uploaded successfully.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ homepage:
|
|||
label: 开启后背景不会随页面滚动而滚动
|
||||
copyright_prefer:
|
||||
title: 程序版权信息
|
||||
description: "每种支持的语言都可以对应不同的程序版权信息,如果想要编辑某种特定语言下的版权信息,请在右上角切换至该语言后再提交修改。<b>对于任何恶意修改页面右下角的版权信息(包括不限于删除、修改作者信息、修改链接指向)的用户,作者保留对其追究责任的权利。</b>"
|
||||
description: '每种支持的语言都可以对应不同的程序版权信息,如果想要编辑某种特定语言下的版权信息,请在右上角切换至该语言后再提交修改。<b>对于任何恶意修改页面右下角的版权信息(包括不限于删除、修改作者信息、修改链接指向)的用户,作者保留对其追究责任的权利。</b>'
|
||||
copyright_text:
|
||||
title: 自定义版权文字
|
||||
description: 自定义版权文字内可使用占位符,<code>{site_name}</code> 将会被自动替换为站点名称,<code>{site_url}</code> 会被替换为站点地址。每种支持的语言都可以对应不同的自定义版权文字,如果想要编辑某种特定语言下的版权文字,请在右上角切换至该语言后再提交修改。
|
||||
|
|
@ -97,7 +97,10 @@ general:
|
|||
regs_per_ip: 每个 IP 限制注册数
|
||||
max_upload_file_size:
|
||||
title: 最大允许上传大小
|
||||
hint: "PHP 限制::size,定义在 php.ini 中。"
|
||||
hint: 'PHP 限制::size,定义在 php.ini 中。'
|
||||
max_texture_width:
|
||||
title: 最大允许材质宽度
|
||||
hint: 允许上传的材质的最大的宽度,必须是 64 的整数倍
|
||||
player_name_rule:
|
||||
title: 角色名规则
|
||||
official: 大小写字母数字下划线(Mojang 官方的用户名规则)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ upload:
|
|||
private-score-notice: 私密材质将会消耗更多的积分:每 KB 存储空间 :score 积分
|
||||
invalid-size: 不是有效的 :type 文件(宽 :width,高 :height)
|
||||
invalid-hd-skin: 不是有效的高清皮肤(宽和高不是 32 的整数倍)
|
||||
too-wide: 材质过宽(:widthpx),本站允许的最大宽度为 :maxWidthpx
|
||||
lack-score: 积分不足
|
||||
repeated: 已经有人上传过这个材质了,直接添加到衣柜使用吧~
|
||||
success: 材质 :name 上传成功
|
||||
|
|
|
|||
|
|
@ -303,6 +303,22 @@ class SkinlibControllerTest extends TestCase
|
|||
'type' => 'steve',
|
||||
])->assertJsonValidationErrors('public');
|
||||
|
||||
// too wide texture
|
||||
option(['max_texture_width' => 128]);
|
||||
$this->postJson(route('texture.upload'), [
|
||||
'name' => 'texture',
|
||||
'file' => UploadedFile::fake()->image('wide.png', 256, 256),
|
||||
'type' => 'steve',
|
||||
'public' => true,
|
||||
])->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('skinlib.upload.too-wide', [
|
||||
'width' => 256,
|
||||
'maxWidth' => 128,
|
||||
]),
|
||||
]);
|
||||
option(['max_texture_width' => 8192]);
|
||||
|
||||
// invalid skin size
|
||||
$this->postJson(route('texture.upload'), [
|
||||
'name' => 'texture',
|
||||
|
|
@ -437,7 +453,7 @@ class SkinlibControllerTest extends TestCase
|
|||
'uploaded_texture_hash',
|
||||
function ($hash, $file) use ($texture) {
|
||||
$this->assertEquals($texture->hash, $hash);
|
||||
$this->assertInstanceOf(UploadedFile::class, $file);
|
||||
$this->assertIsString($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -445,7 +461,7 @@ class SkinlibControllerTest extends TestCase
|
|||
Event::assertDispatched(
|
||||
'texture.uploading',
|
||||
function ($eventName, $payload) use ($texture) {
|
||||
$this->assertInstanceOf(UploadedFile::class, $payload[0]);
|
||||
$this->assertIsString($payload[0]);
|
||||
$this->assertEquals($texture->name, $payload[1]);
|
||||
$this->assertEquals($texture->hash, $payload[2]);
|
||||
|
||||
|
|
@ -456,7 +472,7 @@ class SkinlibControllerTest extends TestCase
|
|||
'texture.uploaded',
|
||||
function ($eventName, $payload) use ($texture) {
|
||||
$this->assertTrue($texture->is($payload[0]));
|
||||
$this->assertInstanceOf(UploadedFile::class, $payload[1]);
|
||||
$this->assertIsString($payload[1]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user